From a55a6ec1ff6ade0cc471f0fc4415c33a341f2a20 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 10 Jul 2021 15:05:18 +0200 Subject: [PATCH] Fix RightToLeft TextWrapping --- src/Avalonia.Visuals/Media/GlyphRun.cs | 2 +- .../TextFormatting/ShapedTextCharacters.cs | 87 +++++++++++++------ .../Media/TextFormatting/TextCharacters.cs | 9 +- .../Media/TextFormatting/TextFormatterImpl.cs | 86 ++++++++++++------ .../TextFormatting/TextFormatterTests.cs | 35 ++++++++ 5 files changed, 161 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 2b787462e4..234122f6f5 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/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 _); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index b304b19910..64befe2e5c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -90,7 +90,9 @@ namespace Avalonia.Media.TextFormatting /// The split result. 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 diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 0779716ec8..cfca8f9ab2 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/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; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 6533c34ba0..df63b00c25 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/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(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); } /// diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 9c2a1953f1..a19f97e74e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/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() + .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 ‑‑auth."