diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 7d4fac337d6..d001f8f3701 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -9,6 +9,8 @@ namespace Avalonia.Media.TextFormatting /// public class TextCharacters : TextRun { + private static char ZeroWidthSpace = '\u200b'; + /// /// Constructs a run for text content from a string. /// @@ -82,7 +84,21 @@ private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count)) + var count = 0; + var codepoints = new CodepointEnumerator(textSpan); + + while(codepoints.MoveNext(out var firstCodepoint) && firstCodepoint.Value == 0) + { + count++; + } + + //Detect null terminator + if (count > 0) + { + return new UnshapedTextRun(new string(ZeroWidthSpace, count).AsMemory(), defaultProperties, biDiLevel); + } + + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out count)) { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); @@ -177,6 +193,12 @@ internal static bool TryGetShapeableLength( var currentCodepoint = currentGrapheme.FirstCodepoint; var currentScript = currentCodepoint.Script; + if(currentCodepoint.Value == 0) + { + //Do not include null terminators + break; + } + if (!currentCodepoint.IsWhiteSpace && defaultGlyphTypeface != null && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 768e8f3a2f3..cedb2f63cf1 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -14,8 +14,6 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - private const uint ZeroWidthSpace = '\u200b'; - private static readonly ConcurrentDictionary s_cachedLanguage = new(); public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) @@ -69,17 +67,7 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)(sourceInfo.Cluster); - - if (glyphIndex == 0) - { - var codepoint = Codepoint.ReadAt(textSpan, glyphCluster, out _); - - if (codepoint.GeneralCategory == GeneralCategory.Control) - { - glyphIndex = options.Typeface.GetGlyph(ZeroWidthSpace); - } - } + var glyphCluster = (int)sourceInfo.Cluster; var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index f933ffb17be..6f8b2c407fd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1374,6 +1374,31 @@ public void Should_GetPreviousCharacterHit_Non_Trailing() } } + [Theory] + [InlineData("\0", 0.0)] + [InlineData("\0\0\0", 0.0)] + [InlineData("\0A\0\0", 7.201171875)] + [InlineData("\0AA\0AA\0", 28.8046875)] + public void Should_Ignore_Null_Terminator(string text, double width) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(text, defaultProperties, true); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + Assert.NotNull(textLine); + + Assert.Equal(width, textLine.Width); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns;