Browse Source

Fix RightToLeft TextWrapping

Benedikt Stebner 4 years ago
parent
commit
a55a6ec1ff

+ 1 - 1
src/Avalonia.Visuals/Media/GlyphRun.cs

@@ -582,7 +582,7 @@ namespace Avalonia.Media
                 {
                     var cluster = _glyphClusters[i];
 
-                    var codepointIndex = cluster - _characters.Start;
+                    var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
 
                     var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
 

+ 61 - 26
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@@ -90,7 +90,9 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>The split result.</returns>
         public SplitTextCharactersResult Split(int length)
         {
-            var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length);
+            var glyphCount = GlyphRun.IsLeftToRight ?
+                GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length) :
+                GlyphRun.FindGlyphIndex(GlyphRun.Characters.End - length);
 
             if (GlyphRun.Characters.Length == length)
             {
@@ -102,31 +104,64 @@ namespace Avalonia.Media.TextFormatting
                 return new SplitTextCharactersResult(this, null);
             }
 
-            var firstGlyphRun = new GlyphRun(
-                Properties.Typeface.GlyphTypeface,
-                Properties.FontRenderingEmSize,
-                GlyphRun.GlyphIndices.Take(glyphCount),
-                GlyphRun.GlyphAdvances.Take(glyphCount),
-                GlyphRun.GlyphOffsets.Take(glyphCount),
-                GlyphRun.Characters.Take(length),
-                GlyphRun.GlyphClusters.Take(glyphCount),
-                GlyphRun.BiDiLevel);
-
-            var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
-
-            var secondGlyphRun = new GlyphRun(
-                Properties.Typeface.GlyphTypeface,
-                Properties.FontRenderingEmSize,
-                GlyphRun.GlyphIndices.Skip(glyphCount),
-                GlyphRun.GlyphAdvances.Skip(glyphCount),
-                GlyphRun.GlyphOffsets.Skip(glyphCount),
-                GlyphRun.Characters.Skip(length),
-                GlyphRun.GlyphClusters.Skip(glyphCount),
-                GlyphRun.BiDiLevel);
-
-            var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
-
-            return new SplitTextCharactersResult(firstTextRun, secondTextRun);
+            if (GlyphRun.IsLeftToRight)
+            {
+                var firstGlyphRun = new GlyphRun(
+                    Properties.Typeface.GlyphTypeface,
+                    Properties.FontRenderingEmSize,
+                    GlyphRun.GlyphIndices.Take(glyphCount),
+                    GlyphRun.GlyphAdvances.Take(glyphCount),
+                    GlyphRun.GlyphOffsets.Take(glyphCount),
+                    GlyphRun.Characters.Take(length),
+                    GlyphRun.GlyphClusters.Take(glyphCount),
+                    GlyphRun.BiDiLevel);
+
+                var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
+
+                var secondGlyphRun = new GlyphRun(
+                    Properties.Typeface.GlyphTypeface,
+                    Properties.FontRenderingEmSize,
+                    GlyphRun.GlyphIndices.Skip(glyphCount),
+                    GlyphRun.GlyphAdvances.Skip(glyphCount),
+                    GlyphRun.GlyphOffsets.Skip(glyphCount),
+                    GlyphRun.Characters.Skip(length),
+                    GlyphRun.GlyphClusters.Skip(glyphCount),
+                    GlyphRun.BiDiLevel);
+
+                var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
+
+                return new SplitTextCharactersResult(firstTextRun, secondTextRun);
+            }
+            else
+            {
+                var take = GlyphRun.GlyphIndices.Length - glyphCount;
+                
+                var firstGlyphRun = new GlyphRun(
+                    Properties.Typeface.GlyphTypeface,
+                    Properties.FontRenderingEmSize,
+                    GlyphRun.GlyphIndices.Take(take),
+                    GlyphRun.GlyphAdvances.Take(take),
+                    GlyphRun.GlyphOffsets.Take(take),
+                    GlyphRun.Characters.Skip(length),
+                    GlyphRun.GlyphClusters.Take(take),
+                    GlyphRun.BiDiLevel);
+
+                var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
+
+                var secondGlyphRun = new GlyphRun(
+                    Properties.Typeface.GlyphTypeface,
+                    Properties.FontRenderingEmSize,
+                    GlyphRun.GlyphIndices.Skip(take),
+                    GlyphRun.GlyphAdvances.Skip(take),
+                    GlyphRun.GlyphOffsets.Skip(take),
+                    GlyphRun.Characters.Take(length),
+                    GlyphRun.GlyphClusters.Skip(take),
+                    GlyphRun.BiDiLevel);
+
+                var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
+
+                return new SplitTextCharactersResult(secondTextRun,firstTextRun);
+            }
         }
 
         public readonly struct SplitTextCharactersResult

+ 6 - 3
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@@ -134,7 +134,7 @@ namespace Avalonia.Media.TextFormatting
             var isFallback = typeface != defaultTypeface;
 
             count = 0;
-            var script = Script.Common;
+            var script = Script.Unknown;
             var direction = BiDiClass.LeftToRight;
 
             var font = typeface.GlyphTypeface;
@@ -161,7 +161,7 @@ namespace Avalonia.Media.TextFormatting
 
                 if (currentScript != script)
                 {
-                    if (script == Script.Inherited || script == Script.Common)
+                    if (script is Script.Unknown)
                     {
                         script = currentScript;
                     }
@@ -174,13 +174,16 @@ namespace Avalonia.Media.TextFormatting
                     }
                 }
 
-                if (currentScript != Script.Common && currentScript != Script.Inherited)
+                //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;

+ 58 - 28
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@@ -70,34 +70,74 @@ namespace Avalonia.Media.TextFormatting
             {
                 var glyphTypeface = glyphRun.GlyphTypeface;
 
-                for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
+                if (glyphRun.IsLeftToRight)
                 {
-                    var glyph = glyphRun.GlyphIndices[i];
+                    foreach (var glyph in glyphRun.GlyphIndices)
+                    {
+                        var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
 
-                    var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
+                        if (currentWidth + advance > availableWidth)
+                        {
+                            break;
+                        }
 
-                    if (currentWidth + advance > availableWidth)
-                    {
-                        break;
+                        currentWidth += advance;
+
+                        glyphCount++;
                     }
+                }
+                else
+                {
+                    for (var index = glyphRun.GlyphClusters.Length - 1; index > 0; index--)
+                    {
+                        var glyph = glyphRun.GlyphIndices[index];
+
+                        var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
 
-                    currentWidth += advance;
+                        if (currentWidth + advance > availableWidth)
+                        {
+                            break;
+                        }
 
-                    glyphCount++;
+                        currentWidth += advance;
+
+                        glyphCount++;
+                    }
                 }
             }
             else
             {
-                foreach (var advance in glyphRun.GlyphAdvances)
+                if (glyphRun.IsLeftToRight)
                 {
-                    if (currentWidth + advance > availableWidth)
+                    for (var index = 0; index < glyphRun.GlyphAdvances.Length; index++)
                     {
-                        break;
+                        var advance = glyphRun.GlyphAdvances[index];
+                    
+                        if (currentWidth + advance > availableWidth)
+                        {
+                            break;
+                        }
+
+                        currentWidth += advance;
+
+                        glyphCount++;
                     }
+                }
+                else
+                {
+                    for (var index = glyphRun.GlyphAdvances.Length - 1; index > 0; index--)
+                    {
+                        var advance = glyphRun.GlyphAdvances[index];
+                    
+                        if (currentWidth + advance > availableWidth)
+                        {
+                            break;
+                        }
 
-                    currentWidth += advance;
+                        currentWidth += advance;
 
-                    glyphCount++;
+                        glyphCount++;
+                    }
                 }
             }
 
@@ -475,24 +515,14 @@ namespace Avalonia.Media.TextFormatting
 
             var remainingCharacters = splitResult.Second;
 
-            if (currentLineBreak?.RemainingCharacters != null)
+            var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null;
+
+            if (lineBreak is null && currentLineBreak.TextEndOfLine != null)
             {
-                if (remainingCharacters != null)
-                {
-                    remainingCharacters.AddRange(currentLineBreak.RemainingCharacters);
-                }
-                else
-                {
-                    remainingCharacters = new List<ShapedTextCharacters>(currentLineBreak.RemainingCharacters);
-                }
+                lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine);
             }
 
-            var lineBreak = remainingCharacters != null && remainingCharacters.Count > 0 ?
-                new TextLineBreak(remainingCharacters) :
-                null;
-
-            return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties,
-                lineBreak);
+            return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak);
         }
 
         /// <summary>

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

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;
@@ -203,6 +204,40 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
                 Assert.Equal(expectedNumberOfLines, numberOfLines);
             }
         }
+        
+        [Fact]
+        public void Should_Wrap_RightToLeft()
+        {
+            using (Start())
+            {
+                const string text =
+                    "قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء";
+                
+                var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+
+                var textSource = new SingleBufferTextSource(text, defaultProperties);
+
+                var formatter = new TextFormatterImpl();
+
+                var currentTextSourceIndex = 0;
+
+                while (currentTextSourceIndex < text.Length)
+                {
+                    var textLine =
+                        formatter.FormatLine(textSource, currentTextSourceIndex, 50,
+                            new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
+
+                    var glyphClusters = textLine.TextRuns.Cast<ShapedTextCharacters>()
+                        .SelectMany(x => x.GlyphRun.GlyphClusters).ToArray();
+                
+                    Assert.True(glyphClusters[0] >= glyphClusters[^1]);
+                    
+                    Assert.Equal(currentTextSourceIndex, glyphClusters[^1]);
+
+                    currentTextSourceIndex += textLine.TextRange.Length;
+                }
+            }
+        }
 
         [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
                     "IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."