diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index ec270d796a..9a2645f03d 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -49,7 +49,7 @@ namespace Avalonia.Media IReadOnlyList? glyphClusters = null, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + _glyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; @@ -204,7 +204,7 @@ namespace Avalonia.Media public double GetDistanceFromCharacterHit(CharacterHit characterHit) { var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - + var distance = 0.0; if (IsLeftToRight) @@ -223,7 +223,7 @@ namespace Avalonia.Media } var glyphIndex = FindGlyphIndex(characterIndex); - + if (GlyphClusters != null) { var currentCluster = GlyphClusters[glyphIndex]; @@ -249,7 +249,7 @@ namespace Avalonia.Media { //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - + if (GlyphClusters != null) { if (characterIndex > GlyphClusters[0]) @@ -284,13 +284,13 @@ namespace Avalonia.Media public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) { var characterIndex = 0; - + // Before if (distance <= 0) { isInside = false; - if(GlyphClusters != null) + if (GlyphClusters != null) { characterIndex = GlyphClusters[characterIndex]; } @@ -307,11 +307,11 @@ namespace Avalonia.Media characterIndex = GlyphIndices.Count - 1; - if(GlyphClusters != null) + if (GlyphClusters != null) { characterIndex = GlyphClusters[characterIndex]; } - + var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _); return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); @@ -327,7 +327,7 @@ namespace Avalonia.Media var advance = GetGlyphAdvance(index, out var cluster); characterIndex = cluster; - + if (distance > currentX && distance <= currentX + advance) { break; @@ -345,7 +345,7 @@ namespace Avalonia.Media var advance = GetGlyphAdvance(index, out var cluster); characterIndex = cluster; - + if (currentX - advance < distance) { break; @@ -554,6 +554,16 @@ namespace Avalonia.Media nextCluster = GlyphClusters[currentIndex]; } + if (nextCluster < Characters.Start) + { + nextCluster = Characters.Start; + } + + if (cluster < Characters.Start) + { + cluster = Characters.Start; + } + int trailingLength; if (nextCluster == cluster) @@ -577,7 +587,7 @@ namespace Avalonia.Media private double GetGlyphAdvance(int index, out int cluster) { cluster = GlyphClusters != null ? GlyphClusters[index] : index; - + if (GlyphAdvances != null) { return GlyphAdvances[index]; @@ -603,7 +613,7 @@ namespace Avalonia.Media var widthIncludingTrailingWhitespace = 0d; var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount); - + for (var index = 0; index < GlyphIndices.Count; index++) { var advance = GetGlyphAdvance(index, out _); @@ -615,7 +625,7 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = GlyphIndices.Count - glyphCount; index { new ShapedTextCharacters(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 1, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); + return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 30e3728d1f..b480774d1d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting case ShapedTextCharacters shapedRun: { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + break; } default: @@ -426,31 +427,42 @@ namespace Avalonia.Media.TextFormatting if (nextRun != null) { - if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End) + switch (nextRun) { - goto skip; - } + case ShapedTextCharacters when currentRun is ShapedTextCharacters: + { + if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End) + { + goto skip; + } - if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength) - { - goto skip; - } + if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength) + { + goto skip; + } - if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex) - { - goto skip; - } + if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex) + { + goto skip; + } - if (currentRun.Text.End < firstTextSourceCharacterIndex) - { - goto skip; - } + if (currentRun.Text.End < firstTextSourceCharacterIndex) + { + goto skip; + } - goto noop; + goto noop; + } + default: + { + goto noop; + } + } skip: { startX += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; } continue; @@ -460,7 +472,6 @@ namespace Avalonia.Media.TextFormatting } } - var endX = startX; var endOffset = 0d; @@ -520,11 +531,13 @@ namespace Avalonia.Media.TextFormatting } default: { - if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length) + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength) { endX += currentRun.Size.Width; } + currentPosition += currentRun.TextSourceLength; + break; } } @@ -538,7 +551,9 @@ namespace Avalonia.Media.TextFormatting if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) { - var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection); + currentRect = currentRect.WithWidth(currentRect.Width + width); + + var textBounds = new TextBounds(currentRect, currentDirection); result[result.Count - 1] = textBounds; } @@ -551,21 +566,9 @@ namespace Avalonia.Media.TextFormatting if (currentDirection == FlowDirection.LeftToRight) { - if (nextRun != null) - { - if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength) - { - break; - } - - currentPosition = nextRun.Text.End; - } - else + if (currentPosition >= firstTextSourceCharacterIndex + textLength) { - if (currentPosition >= firstTextSourceCharacterIndex + textLength) - { - break; - } + break; } } else @@ -575,10 +578,7 @@ namespace Avalonia.Media.TextFormatting break; } - if (currentPosition != currentRun.Text.Start) - { - endX += currentRun.Size.Width - endOffset; - } + endX += currentRun.Size.Width - endOffset; } lastDirection = currentDirection; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index e9bc792be3..b58d9051f3 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } } - + [Fact] public void Should_Get_Next_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; - + using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); @@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting shapedRun.ShapedBuffer.GlyphClusters.Reverse() : shapedRun.ShapedBuffer.GlyphClusters); } - + var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); foreach (var cluster in clusters) @@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public void Should_Get_Previous_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; - + using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); @@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } clusters.Reverse(); - + var nextCharacterHit = new CharacterHit(text.Length - 1); foreach (var cluster in clusters) { var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; - + Assert.Equal(cluster, currentCaretIndex); nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit); @@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); } } - + [InlineData("𐐷𐐷𐐷𐐷𐐷")] [InlineData("01234567🎉\n")] [InlineData("𐐷1234")] @@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); + Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); } } @@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026"); yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026"); yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, ""); - + object[] CreateData(string text, double width, TextTrimming mode, string expected) { return new object[] @@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new DrawableRunTextSource(); - + var formatter = new TextFormatterImpl(); var textLine = @@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(4, textLine.TextRuns.Count); - var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1)); + var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1)); Assert.Equal(3, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); @@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting switch (textSourceIndex) { case 0: - return new CustomDrawableRun(); + return new CustomDrawableRun(); case 1: return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default)); case 2: - return new CustomDrawableRun(); + return new CustomDrawableRun(); case 3: return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default)); default: @@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } } - + private class CustomDrawableRun : DrawableTextRun { public override Size Size => new(14, 14); public override double Baseline => 14; public override void Draw(DrawingContext drawingContext, Point origin) { - + } } @@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedTextRuns = textLine.TextRuns.Cast().ToList(); var lastCluster = -1; - + foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; var currentClusters = shapedBuffer.GlyphClusters.ToList(); - foreach (var currentCluster in currentClusters) + foreach (var currentCluster in currentClusters) { if (lastCluster == currentCluster) { continue; } - + glyphClusters.Add(currentCluster); lastCluster = currentCluster; } } - + return glyphClusters; } - + private static List BuildRects(TextLine textLine) { var rects = new List(); @@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; - + for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) { var currentCluster = shapedBuffer.GlyphClusters[index]; - + var advance = shapedBuffer.GlyphAdvances[index]; if (lastCluster != currentCluster) @@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting rects.Remove(rect); rect = rect.WithWidth(rect.Width + advance); - + rects.Add(rect); } - + currentX += advance; lastCluster = currentCluster; @@ -655,8 +655,43 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return rects; } + [Fact] - public void Should_Get_TextBounds() + public void Should_Get_TextBounds_Mixed() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var text = "0123".AsMemory(); + var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); + + var textRuns = new List + { + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption), defaultProperties), + new CustomDrawableRun(), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 1, text.Length), shaperOption), defaultProperties), + new CustomDrawableRun(), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 2, text.Length), shaperOption), defaultProperties), + new CustomDrawableRun(), + }; + + var textSource = new FixedRunsTextSource(textRuns); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + } + } + + [Fact] + public void Should_Get_TextBounds_BiDi() { using (Start()) { @@ -673,7 +708,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 3, text.Length), ltrOptions), defaultProperties) }; - + var textSource = new FixedRunsTextSource(textRuns); var formatter = new TextFormatterImpl(); @@ -700,12 +735,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public TextRun? GetTextRun(int textSourceIndex) { + var currentPosition = 0; + foreach (var textRun in _textRuns) { - if(textRun.Text.Start == textSourceIndex) + if (currentPosition == textSourceIndex) { return textRun; } + + currentPosition += textRun.TextSourceLength; } return null;