diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 2b787462e4e..234122f6f5d 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -582,7 +582,7 @@ private int GetTrailingWhitespaceLength(out int newLineLength) { 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 b304b19910a..64befe2e5cb 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -90,7 +90,9 @@ public override void Draw(DrawingContext drawingContext, Point origin) /// 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 @@ public SplitTextCharactersResult Split(int length) 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 0779716ec8b..cfca8f9ab2a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -134,7 +134,7 @@ protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, 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 @@ protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, if (currentScript != script) { - if (script == Script.Inherited || script == Script.Common) + if (script is Script.Unknown) { script = currentScript; } @@ -174,13 +174,16 @@ protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, } } - 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 6533c34ba0a..df63b00c25c 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -70,34 +70,74 @@ internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, d { 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 @@ private static TextLine PerformTextWrapping(List textRuns, 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 9c2a1953f17..a19f97e74eb 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 @@ public void Should_Wrap_With_Overflow(string text, int expectedCharactersPerLine 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."