diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f426a20b2c..f5812f71ff 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -697,13 +697,18 @@ namespace Avalonia.Media.TextFormatting i = lastRunIndex; + //Possible overlap at runs of different direction if (directionalWidth == 0) { - continue; + //In case a run only contains a linebreak we don't want to skip it. + if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } } - var coveredLength = 0; - TextBounds? textBounds = null; + int coveredLength; + TextBounds? textBounds; switch (currentDirection) { @@ -831,14 +836,18 @@ namespace Avalonia.Media.TextFormatting i = firstRunIndex; + //Possible overlap at runs of different direction if (directionalWidth == 0) { - continue; + //In case a run only contains a linebreak we don't want to skip it. + if (currentRun is ShapedTextRun shaped && currentRun.Length - shaped.GlyphRun.Metrics.NewLineLength > 0) + { + continue; + } } - var coveredLength = 0; - TextBounds? textBounds = null; + int coveredLength; switch (currentDirection) { diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 7d7be46c72..aeb9025ade 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -26,8 +26,8 @@ namespace Avalonia.Skia using (var buffer = new Buffer()) { // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length); - buffer.AddUtf16(containingText.Span, start, length); + var containingText = GetContainingMemory(text, out var start, out var length).Span; + buffer.AddUtf16(containingText, start, length); MergeBreakPair(buffer); @@ -72,7 +72,7 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (i < textSpan.Length && textSpan[i] == '\t') + if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 36897d3aff..b881129488 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; @@ -13,6 +14,8 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { + private static readonly ConcurrentDictionary s_cachedLanguage = new(); + public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; @@ -24,8 +27,8 @@ namespace Avalonia.Direct2D1.Media using (var buffer = new Buffer()) { // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length); - buffer.AddUtf16(containingText.Span, start, length); + var containingText = GetContainingMemory(text, out var start, out var length).Span; + buffer.AddUtf16(containingText, start, length); MergeBreakPair(buffer); @@ -33,7 +36,9 @@ namespace Avalonia.Direct2D1.Media buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + var usedCulture = culture ?? CultureInfo.CurrentCulture; + + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); var font = ((GlyphTypefaceImpl)typeface).Font; @@ -68,7 +73,7 @@ namespace Avalonia.Direct2D1.Media var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (i < textSpan.Length && textSpan[i] == '\t') + if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index a24da38fdd..c84fcaaa9a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1051,7 +1051,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")] [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")] [Theory] - public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, + public void Should_HitTestTextRangeBetweenRuns(string text, int start, int length, FlowDirection flowDirection, string expected) { using (Start()) @@ -1087,6 +1087,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_HitTestTextRangeWithLineBreaks() + { + using (Start()) + { + var beforeLinebreak = "Line before linebreak"; + var afterLinebreak = "Line after linebreak"; + var text = beforeLinebreak + Environment.NewLine + "" + Environment.NewLine + afterLinebreak; + + var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black); + + var end = text.Length - afterLinebreak.Length + 1; + + var rects = textLayout.HitTestTextRange(0, end).ToArray(); + + Assert.Equal(3, rects.Length); + + var endX = textLayout.TextLines[2].GetDistanceFromCharacterHit(new CharacterHit(end)); + + //First character should be covered + Assert.Equal(7.201171875, endX, 2); + } + } + private static IDisposable Start() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 0cfa78d007..0ddcb2452d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -622,6 +622,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Get_TextBounds_For_LineBreak() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(Environment.NewLine, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textBounds = textLine.GetTextBounds(0, Environment.NewLine.Length); + + Assert.Equal(1, textBounds.Count); + + Assert.Equal(1, textBounds[0].TextRunBounds.Count); + + Assert.Equal(Environment.NewLine.Length, textBounds[0].TextRunBounds[0].Length); + } + } + [Fact] public void Should_GetTextRange() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 92778793c2..b748f8fc58 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -31,11 +31,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\t"; + var text = "012345\t"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); + var shapedBuffer = TextShaper.Current.ShapeText(text.AsMemory().Slice(6), options); - Assert.Equal(shapedBuffer.Length, text.Length); + Assert.Equal(1, shapedBuffer.Length); Assert.Equal(100, shapedBuffer[0].GlyphAdvance); } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 375f87259d..98f188f15a 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Globalization; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting; @@ -13,6 +14,7 @@ namespace Avalonia.UnitTests { internal class HarfBuzzTextShaperImpl : ITextShaperImpl { + private static readonly ConcurrentDictionary s_cachedLanguage = new(); public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { var textSpan = text.Span; @@ -24,8 +26,8 @@ namespace Avalonia.UnitTests using (var buffer = new Buffer()) { // HarfBuzz needs the surrounding characters to correctly shape the text - var containingText = GetContainingMemory(text, out var start, out var length); - buffer.AddUtf16(containingText.Span, start, length); + var containingText = GetContainingMemory(text, out var start, out var length).Span; + buffer.AddUtf16(containingText, start, length); MergeBreakPair(buffer); @@ -33,7 +35,9 @@ namespace Avalonia.UnitTests buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + var usedCulture = culture ?? CultureInfo.CurrentCulture; + + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font; @@ -68,7 +72,7 @@ namespace Avalonia.UnitTests var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (textSpan[i] == '\t') + if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') { glyphIndex = typeface.GetGlyph(' ');