Browse Source

Merge branch 'feature/relative-panel' of https://github.com/AvaloniaUI/Avalonia into feature/relative-panel

Dan Walmsley 5 years ago
parent
commit
54a9e99bb1
26 changed files with 305 additions and 156 deletions
  1. 2 2
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  2. 2 4
      src/Avalonia.Controls/Primitives/AccessText.cs
  3. 26 5
      src/Avalonia.Controls/TextBlock.cs
  4. 5 5
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  5. 8 6
      src/Avalonia.Visuals/Media/FontManager.cs
  6. 1 1
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  7. 1 10
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  8. 1 1
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  9. 66 3
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  10. 17 20
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  11. 1 2
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  12. 5 12
      src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
  13. 3 2
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  14. 4 3
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  15. 3 2
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  16. 28 21
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  17. 5 3
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  18. 47 46
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  19. 3 2
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  20. 1 1
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  21. 3 3
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  22. 33 0
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  23. 1 1
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  24. 1 1
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  25. 1 0
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  26. 37 0
      tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs

+ 2 - 2
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -273,7 +273,7 @@ namespace Avalonia.Controls.Presenters
             return new FormattedText
             {
                 Constraint = constraint,
-                Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
+                Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
                 FontSize = FontSize,
                 Text = text ?? string.Empty,
                 TextAlignment = TextAlignment,
@@ -490,7 +490,7 @@ namespace Avalonia.Controls.Presenters
                 return new FormattedText
                 {
                     Text = "X",
-                    Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
+                    Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
                     FontSize = FontSize,
                     TextAlignment = TextAlignment,
                     Constraint = availableSize,

+ 2 - 4
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -97,9 +97,7 @@ namespace Avalonia.Controls.Primitives
             {
                 var lastLine = TextLayout.TextLines[TextLayout.TextLines.Count - 1];
 
-                var offsetX = lastLine.LineMetrics.BaselineOrigin.X;
-
-                var lineX = offsetX + lastLine.LineMetrics.Size.Width;
+                var lineX = lastLine.LineMetrics.Size.Width;
 
                 var lineY = Bounds.Height - lastLine.LineMetrics.Size.Height;
 
@@ -117,7 +115,7 @@ namespace Avalonia.Controls.Primitives
                     continue;
                 }
 
-                var currentX = textLine.LineMetrics.BaselineOrigin.X;
+                var currentX = 0.0;
 
                 foreach (var textRun in textLine.TextRuns)
                 {

+ 26 - 5
src/Avalonia.Controls/TextBlock.cs

@@ -411,9 +411,30 @@ namespace Avalonia.Controls
                 context.FillRectangle(background, new Rect(Bounds.Size));
             }
 
+            if (TextLayout is null)
+            {
+                return;
+            }
+
+            var textAlignment = TextAlignment;
+
+            var width = Bounds.Size.Width;
+
+            var offsetX = 0.0;
+
+            switch (textAlignment)
+            {
+                case TextAlignment.Center:
+                    offsetX = (width - TextLayout.Size.Width) / 2;
+                    break;
+                case TextAlignment.Right:
+                    offsetX =  width - TextLayout.Size.Width;
+                    break;
+            }
+
             var padding = Padding;
 
-            TextLayout?.Draw(context, new Point(padding.Left, padding.Top));
+            TextLayout.Draw(context, new Point(padding.Left + offsetX, padding.Top));
         }
 
         /// <summary>
@@ -431,7 +452,7 @@ namespace Avalonia.Controls
 
             return new TextLayout(
                 text ?? string.Empty,
-                FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
+                FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
                 FontSize,
                 Foreground,
                 TextAlignment,
@@ -470,12 +491,12 @@ namespace Avalonia.Controls
 
             if (_constraint != availableSize)
             {
+                _constraint = availableSize;
+
                 InvalidateTextLayout();
             }
 
-            _constraint = availableSize;
-
-            var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty;
+            var measuredSize = TextLayout?.Size ?? Size.Empty;
 
             return measuredSize.Inflate(padding);
         }

+ 5 - 5
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -54,7 +54,7 @@ namespace Avalonia.Headless
 
     class HeadlessCursorFactoryStub : IStandardCursorFactory
     {
-        
+
         public IPlatformHandle GetCursor(StandardCursorType cursorType)
         {
             return new PlatformHandle(new IntPtr((int)cursorType), "STUB");
@@ -101,7 +101,7 @@ namespace Avalonia.Headless
         public bool IsFixedPitch => true;
 
         public void Dispose()
-        {            
+        {
         }
 
         public ushort GetGlyph(uint codepoint)
@@ -155,9 +155,9 @@ namespace Avalonia.Headless
             return new List<string> { "Arial" };
         }
 
-        public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
         {
-            fontKey = new FontKey("Arial", fontWeight, fontStyle);
+            fontKey = new FontKey("Arial", fontStyle, fontWeight);
             return true;
         }
     }
@@ -169,7 +169,7 @@ namespace Avalonia.Headless
         {
             public void Save(Stream outputStream)
             {
-                
+
             }
         }
         public IWindowIconImpl LoadIcon(string fileName)

+ 8 - 6
src/Avalonia.Visuals/Media/FontManager.cs

@@ -79,12 +79,13 @@ namespace Avalonia.Media
         ///     Returns a new typeface, or an existing one if a matching typeface exists.
         /// </summary>
         /// <param name="fontFamily">The font family.</param>
-        /// <param name="fontWeight">The font weight.</param>
         /// <param name="fontStyle">The font style.</param>
+        /// <param name="fontWeight">The font weight.</param>
         /// <returns>
         ///     The typeface.
         /// </returns>
-        public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal)
+        public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal,
+            FontWeight fontWeight = FontWeight.Normal)
         {
             while (true)
             {
@@ -93,7 +94,7 @@ namespace Avalonia.Media
                     fontFamily = _defaultFontFamily;
                 }
 
-                var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
+                var key = new FontKey(fontFamily.Name, fontStyle, fontWeight);
 
                 if (_typefaceCache.TryGetValue(key, out var typeface))
                 {
@@ -121,15 +122,16 @@ namespace Avalonia.Media
         ///     Returns <c>null</c> if no fallback was found.
         /// </summary>
         /// <param name="codepoint">The codepoint to match against.</param>
-        /// <param name="fontWeight">The font weight.</param>
         /// <param name="fontStyle">The font style.</param>
+        /// <param name="fontWeight">The font weight.</param>
         /// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
         /// <param name="culture">The culture.</param>
         /// <returns>
         ///     The matched typeface.
         /// </returns>
-        public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal,
+        public Typeface MatchCharacter(int codepoint,
             FontStyle fontStyle = FontStyle.Normal,
+            FontWeight fontWeight = FontWeight.Normal,
             FontFamily fontFamily = null, CultureInfo culture = null)
         {
             foreach (var cachedTypeface in _typefaceCache.Values)
@@ -142,7 +144,7 @@ namespace Avalonia.Media
                 }
             }
 
-            var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
+            var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ?
                 _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) :
                 null;
 

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

@@ -4,7 +4,7 @@ namespace Avalonia.Media.Fonts
 {
     public readonly struct FontKey : IEquatable<FontKey>
     {
-        public FontKey(string familyName, FontWeight weight, FontStyle style)
+        public FontKey(string familyName, FontStyle style, FontWeight weight)
         {
             FamilyName = familyName;
             Style = style;

+ 1 - 10
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@@ -89,16 +89,7 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>The split result.</returns>
         public SplitTextCharactersResult Split(int length)
         {
-            var glyphCount = 0;
-
-            var firstCharacters = GlyphRun.Characters.Take(length);
-
-            var codepointEnumerator = new CodepointEnumerator(firstCharacters);
-
-            while (codepointEnumerator.MoveNext())
-            {
-                glyphCount++;
-            }
+            var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length);
 
             if (GlyphRun.Characters.Length == length)
             {

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

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

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

@@ -72,6 +72,11 @@ namespace Avalonia.Media.TextFormatting
             {
                 foreach (var shapedCharacters in previousLineBreak.RemainingCharacters)
                 {
+                    if (shapedCharacters == null)
+                    {
+                        continue;
+                    }
+
                     textRuns.Add(shapedCharacters);
 
                     if (TryGetLineBreak(shapedCharacters, out var runLineBreak))
@@ -106,7 +111,7 @@ namespace Avalonia.Media.TextFormatting
                                 var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
                                     run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
 
-                                var shapedCharacters = new ShapedTextCharacters(glyphRun, textRun.Properties);
+                                var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
 
                                 textRuns.Add(shapedCharacters);
                             }
@@ -355,9 +360,67 @@ namespace Avalonia.Media.TextFormatting
         {
             var glyphRun = textCharacters.GlyphRun;
 
-            var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _);
+            if (glyphRun.Bounds.Width < availableWidth)
+            {
+                return glyphRun.Characters.Length;
+            }
+
+            var glyphCount = 0;
+
+            var currentWidth = 0.0;
+
+            if (glyphRun.GlyphAdvances.IsEmpty)
+            {
+                var glyphTypeface = glyphRun.GlyphTypeface;
+
+                for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
+                {
+                    var glyph = glyphRun.GlyphIndices[i];
+
+                    var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
+
+                    if (currentWidth + advance > availableWidth)
+                    {
+                        break;
+                    }
+
+                    currentWidth += advance;
+
+                    glyphCount++;
+                }
+            }
+            else
+            {
+                for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++)
+                {
+                    var advance = glyphRun.GlyphAdvances[i];
+
+                    if (currentWidth + advance > availableWidth)
+                    {
+                        break;
+                    }
+
+                    currentWidth += advance;
+
+                    glyphCount++;
+                }
+            }
+
+            if (glyphCount == glyphRun.GlyphIndices.Length)
+            {
+                return glyphRun.Characters.Length;
+            }
+
+            if (glyphRun.GlyphClusters.IsEmpty)
+            {
+                return glyphCount;
+            }
+
+            var firstCluster = glyphRun.GlyphClusters[0];
+
+            var lastCluster = glyphRun.GlyphClusters[glyphCount];
 
-            return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textCharacters.Text.Start;
+            return lastCluster - firstCluster;
         }
 
         /// <summary>

+ 17 - 20
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -103,12 +103,12 @@ namespace Avalonia.Media.TextFormatting
         public IReadOnlyList<TextLine> TextLines { get; private set; }
 
         /// <summary>
-        /// Gets the bounds of the layout.
+        /// Gets the size of the layout.
         /// </summary>
         /// <value>
         /// The bounds.
         /// </value>
-        public Rect Bounds { get; private set; }
+        public Size Size { get; private set; }
 
         /// <summary>
         /// Draws the text layout.
@@ -126,7 +126,10 @@ namespace Avalonia.Media.TextFormatting
 
             foreach (var textLine in TextLines)
             {
-                textLine.Draw(context, new Point(origin.X, currentY));
+                var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width,
+                    _paragraphProperties.TextAlignment);
+
+                textLine.Draw(context, new Point(origin.X + offsetX, currentY));
 
                 currentY += textLine.LineMetrics.Size.Height;
             }
@@ -158,22 +161,16 @@ namespace Avalonia.Media.TextFormatting
         /// Updates the current bounds.
         /// </summary>
         /// <param name="textLine">The text line.</param>
-        /// <param name="left">The left.</param>
-        /// <param name="right">The right.</param>
-        /// <param name="bottom">The bottom.</param>
-        private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom)
+        /// <param name="width">The current width.</param>
+        /// <param name="height">The current height.</param>
+        private static void UpdateBounds(TextLine textLine, ref double width, ref double height)
         {
-            if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width)
-            {
-                right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width;
-            }
-
-            if (left < textLine.LineMetrics.BaselineOrigin.X)
+            if (width < textLine.LineMetrics.Size.Width)
             {
-                left = textLine.LineMetrics.BaselineOrigin.X;
+                width = textLine.LineMetrics.Size.Width;
             }
 
-            bottom += textLine.LineMetrics.Size.Height;
+            height += textLine.LineMetrics.Size.Height;
         }
 
         /// <summary>
@@ -204,13 +201,13 @@ namespace Avalonia.Media.TextFormatting
 
                 TextLines = new List<TextLine> { textLine };
 
-                Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height);
+                Size = new Size(0, textLine.LineMetrics.Size.Height);
             }
             else
             {
                 var textLines = new List<TextLine>();
 
-                double left = 0.0, right = 0.0, bottom = 0.0;
+                double width = 0.0, height = 0.0;
 
                 var currentPosition = 0;
 
@@ -228,9 +225,9 @@ namespace Avalonia.Media.TextFormatting
 
                     textLines.Add(textLine);
 
-                    UpdateBounds(textLine, ref left, ref right, ref bottom);
+                    UpdateBounds(textLine, ref width, ref height);
 
-                    if (!double.IsPositiveInfinity(MaxHeight) && bottom > MaxHeight)
+                    if (!double.IsPositiveInfinity(MaxHeight) && height > MaxHeight)
                     {
                         break;
                     }
@@ -247,7 +244,7 @@ namespace Avalonia.Media.TextFormatting
                     textLines.Add(emptyTextLine);
                 }
 
-                Bounds = new Rect(left, 0, right, bottom);
+                Size = new Size(width, height);
 
                 TextLines = textLines;
             }

+ 1 - 2
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@@ -33,8 +33,7 @@ namespace Avalonia.Media.TextFormatting
 
             foreach (var textRun in _textRuns)
             {
-                var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X,
-                    origin.Y + LineMetrics.BaselineOrigin.Y);
+                var baselineOrigin = new Point(currentX, origin.Y + LineMetrics.TextBaseline);
 
                 textRun.Draw(drawingContext, baselineOrigin);
 

+ 5 - 12
src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs

@@ -9,10 +9,10 @@ namespace Avalonia.Media.TextFormatting
     /// </summary>
     public readonly struct TextLineMetrics
     {
-        public TextLineMetrics(Size size, Point baselineOrigin, TextRange textRange)
+        public TextLineMetrics(Size size, double textBaseline, TextRange textRange)
         {
             Size = size;
-            BaselineOrigin = baselineOrigin;
+            TextBaseline = textBaseline;
             TextRange = textRange;
         }
 
@@ -33,12 +33,9 @@ namespace Avalonia.Media.TextFormatting
         public Size Size { get; }
 
         /// <summary>
-        /// Gets the baseline origin.
+        /// Gets the distance from the top to the baseline of the line of text.
         /// </summary>
-        /// <value>
-        /// The baseline origin.
-        /// </value>
-        public Point BaselineOrigin { get; }
+        public double TextBaseline { get; }
 
         /// <summary>
         /// Creates the text line metrics.
@@ -81,16 +78,12 @@ namespace Avalonia.Media.TextFormatting
                 }
             }
 
-            var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, paragraphProperties.TextAlignment);
-
-            var baselineOrigin = new Point(xOrigin, -ascent);
-
             var size = new Size(lineWidth,
                 double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ?
                     descent - ascent + lineGap :
                     paragraphProperties.LineHeight);
 
-            return new TextLineMetrics(size, baselineOrigin, textRange);
+            return new TextLineMetrics(size, -ascent, textRange);
         }
     }
 }

+ 3 - 2
src/Avalonia.Visuals/Platform/IFontManagerImpl.cs

@@ -22,15 +22,16 @@ namespace Avalonia.Platform
         ///     Tries to match a specified character to a typeface that supports specified font properties.
         /// </summary>
         /// <param name="codepoint">The codepoint to match against.</param>
-        /// <param name="fontWeight">The font weight.</param>
         /// <param name="fontStyle">The font style.</param>
+        /// <param name="fontWeight">The font weight.</param>
         /// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
         /// <param name="culture">The culture.</param>
         /// <param name="fontKey">The matching font key.</param>
         /// <returns>
         ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
         /// </returns>
-        bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
+        bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
+            FontWeight fontWeight,
             FontFamily fontFamily, CultureInfo culture, out FontKey fontKey);
 
         /// <summary>

+ 4 - 3
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -29,7 +29,8 @@ namespace Avalonia.Skia
 
         [ThreadStatic] private static string[] t_languageTagBuffer;
 
-        public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
+            FontWeight fontWeight,
             FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
         {
             SKFontStyle skFontStyle;
@@ -80,7 +81,7 @@ namespace Avalonia.Skia
                         continue;
                     }
 
-                    fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
+                    fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight);
 
                     return true;
                 }
@@ -91,7 +92,7 @@ namespace Avalonia.Skia
 
                 if (skTypeface != null)
                 {
-                    fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
+                    fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight);
 
                     return true;
                 }

+ 3 - 2
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Platform;
+using JetBrains.Annotations;
 using SkiaSharp;
 
 namespace Avalonia.Skia
@@ -7,9 +8,9 @@ namespace Avalonia.Skia
     /// <inheritdoc />
     public class GlyphRunImpl : IGlyphRunImpl
     {
-        public GlyphRunImpl(SKTextBlob textBlob)
+        public GlyphRunImpl([NotNull] SKTextBlob textBlob)
         {
-            TextBlob = textBlob;
+            TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob));
         }
 
         /// <summary>

+ 28 - 21
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@@ -19,42 +19,49 @@ namespace Avalonia.Skia
 
         public SKTypeface Get(Typeface typeface)
         {
-            var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style);
+            var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight);
 
             return GetNearestMatch(_typefaces, key);
         }
 
         private static SKTypeface GetNearestMatch(IDictionary<FontKey, SKTypeface> typefaces, FontKey key)
         {
-            if (typefaces.ContainsKey(key))
+            if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface))
             {
-                return typefaces[key];
+                return typeface;
             }
 
-            var keys = typefaces.Keys.Where(
-                x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray();
+            var weight = (int)key.Weight;
 
-            if (!keys.Any())
-            {
-                keys = typefaces.Keys.Where(
-                    x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
+            weight -= weight % 100; // make sure we start at a full weight
 
-                if (!keys.Any())
+            for (var i = (int)key.Style; i < 2; i++)
+            {
+                // only try 2 font weights in each direction
+                for (var j = 0; j < 200; j += 100)
                 {
-                    keys = typefaces.Keys.Where(
-                        x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
-                             (x.Style >= key.Style || x.Style < key.Style)).ToArray();
-                }
-            }
+                    if (weight - j >= 100)
+                    {
+                        if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface))
+                        {
+                            return typeface;
+                        }
+                    }
 
-            if (keys.Length == 0)
-            {
-                return null;
-            }
+                    if (weight + j > 900)
+                    {
+                        continue;
+                    }
 
-            key = keys[0];
+                    if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface))
+                    {
+                        return typeface;
+                    }
+                }
+            }
 
-            return typefaces[key];
+            //Nothing was found so we use the first typeface we can get.
+            return typefaces.Values.FirstOrDefault();
         }
     }
 }

+ 5 - 3
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@@ -43,18 +43,20 @@ namespace Avalonia.Skia
             {
                 var assetStream = assetLoader.Open(asset);
 
-                if (assetStream == null) throw new InvalidOperationException("Asset could not be loaded.");
+                if (assetStream == null)
+                    throw new InvalidOperationException("Asset could not be loaded.");
 
                 var typeface = SKTypeface.FromStream(assetStream);
 
-                if(typeface == null) throw new InvalidOperationException("Typeface could not be loaded.");
+                if (typeface == null)
+                    throw new InvalidOperationException("Typeface could not be loaded.");
 
                 if (typeface.FamilyName != fontFamily.Name)
                 {
                     continue;
                 }
 
-                var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
+                var key = new FontKey(fontFamily.Name, (FontStyle)typeface.FontSlant, (FontWeight)typeface.FontWeight);
 
                 typeFaceCollection.AddTypeface(key, typeface);
             }

+ 47 - 46
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@@ -15,51 +15,7 @@ namespace Avalonia.Skia
         {
             using (var buffer = new Buffer())
             {
-                buffer.ContentType = ContentType.Unicode;
-
-                var breakCharPosition = text.Length - 1;
-
-                var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count);
-
-                if (codepoint.IsBreakChar)
-                {
-                    var breakCharCount = 1;
-
-                    if (text.Length > 1)
-                    {
-                        var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _);
-
-                        if (codepoint == '\r' && previousCodepoint == '\n'
-                            || codepoint == '\n' && previousCodepoint == '\r')
-                        {
-                            breakCharCount = 2;
-                        }
-                    }
-
-                    if (breakCharPosition != text.Start)
-                    {
-                        buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount));
-                    }
-
-                    var cluster = buffer.GlyphInfos.Length > 0 ?
-                        buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 :
-                        (uint)text.Start;
-
-                    switch (breakCharCount)
-                    {
-                        case 1:
-                            buffer.Add('\u200C', cluster);
-                            break;
-                        case 2:
-                            buffer.Add('\u200C', cluster);
-                            buffer.Add('\u200D', cluster);
-                            break;
-                    }
-                }
-                else
-                {
-                    buffer.AddUtf16(text.Buffer.Span);
-                }
+                FillBuffer(buffer, text);
 
                 buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
 
@@ -93,7 +49,7 @@ namespace Avalonia.Skia
                 {
                     glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;
 
-                    clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster);
+                    clusters[i] = (ushort)glyphInfos[i].Cluster;
 
                     if (!glyphTypeface.IsFixedPitch)
                     {
@@ -112,6 +68,51 @@ namespace Avalonia.Skia
             }
         }
 
+        private static void FillBuffer(Buffer buffer, ReadOnlySlice<char> text)
+        {
+            buffer.ContentType = ContentType.Unicode;
+
+            var i = 0;
+
+            while (i < text.Length)
+            {
+                var codepoint = Codepoint.ReadAt(text, i, out var count);
+
+                var cluster = (uint)(text.Start + i);
+
+                if (codepoint.IsBreakChar)
+                {
+                    if (i < text.End)
+                    {
+                        var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _);
+
+                        if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r')
+                        {
+                            count++;
+
+                            buffer.Add('\u200C', cluster);
+
+                            buffer.Add('\u200D', cluster);
+                        }
+                        else
+                        {
+                            buffer.Add('\u200C', cluster);
+                        }
+                    }
+                    else
+                    {
+                        buffer.Add('\u200C', cluster);
+                    }
+                }
+                else
+                {
+                    buffer.Add(codepoint, cluster);
+                }
+
+                i += count;
+            }
+        }
+
         private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
             ref Vector[] offsetBuffer)
         {

+ 3 - 2
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@@ -32,7 +32,8 @@ namespace Avalonia.Direct2D1.Media
             return fontFamilies;
         }
 
-        public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
+            FontWeight fontWeight,
             FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
         {
             var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@@ -50,7 +51,7 @@ namespace Avalonia.Direct2D1.Media
 
                 var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
 
-                fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle);
+                fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight);
 
                 return true;
             }

+ 1 - 1
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         {
             var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
             return r.CreateFormattedText(text,
-                FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle),
+                FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight),
                 fontSize,
                 textAlignment,
                 wrapping,

+ 3 - 3
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@@ -38,7 +38,7 @@ namespace Avalonia.Skia.UnitTests.Media
 
         private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
 
-        public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
             CultureInfo culture, out FontKey fontKey)
         {
             foreach (var customTypeface in _customTypefaces)
@@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media
                     continue;
                 }
 
-                fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle);
+                fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight);
 
                 return true;
             }
@@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media
             var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
                 SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
 
-            fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle);
+            fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight);
 
             return true;
         }

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

@@ -260,6 +260,39 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
             }
         }
 
+        [Fact]
+        public void Should_Not_Produce_TextLine_Wider_Than_ParagraphWidth()
+        {
+            using (Start())
+            {
+                const string text =
+                    "Multiline TextBlock with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
+                    "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. " +
+                    "Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. " +
+                    "Vivamus pretium ornare est.";
+
+                var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+                var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap);
+
+                var textSource = new SingleBufferTextSource(text, defaultProperties);
+
+                var formatter = new TextFormatterImpl();
+
+                var textSourceIndex = 0;
+
+                while (textSourceIndex < text.Length)
+                {
+                    var textLine =
+                        formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties);
+
+                    Assert.True(textLine.LineMetrics.Size.Width <= 200);
+
+                    textSourceIndex += textLine.TextRange.Length;
+                }
+            }
+        }
+
         public static IDisposable Start()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

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

@@ -512,7 +512,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
 
                 Assert.Equal(numberOfLines, layout.TextLines.Count);
 
-                Assert.Equal(numberOfLines * lineHeight, layout.Bounds.Height);
+                Assert.Equal(numberOfLines * lineHeight, layout.Size.Height);
             }
         }
 

+ 1 - 1
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@@ -25,7 +25,7 @@ namespace Avalonia.UnitTests
             return new[] { _defaultFamilyName };
         }
 
-        public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
             CultureInfo culture, out FontKey fontKey)
         {
             fontKey = default;

+ 1 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -34,6 +34,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                         Background = Brushes.Red,
                         Child = textBlock = new TextBlock
                         {
+                            TextWrapping = TextWrapping.NoWrap,
                             Text = "Hello World",
                         }
                     }

+ 37 - 0
tests/Avalonia.Visuals.UnitTests/Utilities/ReadOnlySpanTests.cs

@@ -0,0 +1,37 @@
+using System.Linq;
+using Avalonia.Utilities;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Utilities
+{
+    public class ReadOnlySpanTests
+    {
+        [Fact]
+        public void Should_Skip()
+        {
+            var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+
+            var slice = new ReadOnlySlice<int>(buffer);
+
+            var skipped = slice.Skip(2);
+
+            var expected = buffer.Skip(2);
+
+            Assert.Equal(expected, skipped);
+        }
+
+        [Fact]
+        public void Should_Take()
+        {
+            var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+
+            var slice = new ReadOnlySlice<int>(buffer);
+
+            var taken = slice.Take(8);
+
+            var expected = buffer.Take(8);
+
+            Assert.Equal(expected, taken);
+        }
+    }
+}