public void Should_Enumerate(string text, int expectedLength)
        {
            var enumerator = new GraphemeEnumerator(text.AsMemory());

            Assert.True(enumerator.MoveNext());

            Assert.Equal(expectedLength, enumerator.Current.Text.Length);
        }
Beispiel #2
0
        public void Should_Get_CharacterHit_From_Distance_RTL_With_TextStyles()
        {
            using (Start())
            {
                var text = "أَبْجَدِيَّة عَرَبِيَّة";

                var i = 0;

                var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory());

                while (graphemeEnumerator.MoveNext())
                {
                    var grapheme = graphemeEnumerator.Current;

                    var textStyleOverrides = new[] { new ValueSpan <TextRunProperties>(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };

                    i += grapheme.Text.Length;

                    var layout = new TextLayout(
                        text,
                        Typeface.Default,
                        12,
                        Brushes.Black,
                        textStyleOverrides: textStyleOverrides);

                    var textLine = layout.TextLines[0];

                    var shapedRuns = textLine.TextRuns.Cast <ShapedTextCharacters>().ToList();

                    var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList();

                    var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList();

                    var currentX = 0.0;

                    var cluster = text.Length;

                    for (int j = 0; j < clusters.Count - 1; j++)
                    {
                        var glyphAdvance = glyphAdvances[j];

                        var characterHit = textLine.GetCharacterHitFromDistance(currentX);

                        Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);

                        var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));

                        Assert.Equal(currentX, distance, 5);

                        currentX += glyphAdvance;

                        cluster = clusters[j];
                    }
                }
            }
        }
Beispiel #3
0
        /// <summary>
        /// Tries to get a shapeable length that is supported by the specified typeface.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <param name="typeface">The typeface that is used to find matching characters.</param>
        /// <param name="defaultTypeface"></param>
        /// <param name="length">The shapeable length.</param>
        /// <param name="script"></param>
        /// <returns></returns>
        protected static bool TryGetShapeableLength(
            ReadOnlySlice <char> text,
            Typeface typeface,
            Typeface?defaultTypeface,
            out int length,
            out Script script)
        {
            length = 0;
            script = Script.Unknown;

            if (text.Length == 0)
            {
                return(false);
            }

            var font        = typeface.GlyphTypeface;
            var defaultFont = defaultTypeface?.GlyphTypeface;

            var enumerator = new GraphemeEnumerator(text);

            while (enumerator.MoveNext())
            {
                var currentGrapheme = enumerator.Current;

                var currentScript = currentGrapheme.FirstCodepoint.Script;

                if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
                {
                    break;
                }

                //Stop at the first missing glyph
                if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
                {
                    break;
                }

                if (currentScript != script)
                {
                    if (script is Script.Unknown || currentScript != Script.Common &&
                        script is Script.Common or Script.Inherited)
                    {
                        script = currentScript;
                    }
                    else
                    {
                        if (currentScript != Script.Inherited && currentScript != Script.Common)
                        {
                            break;
                        }
                    }
                }

                length += currentGrapheme.Text.Length;
            }
Beispiel #4
0
        /// <summary>
        /// Creates a shapeable text run with unique properties.
        /// </summary>
        /// <param name="text">The text to create text runs from.</param>
        /// <param name="defaultProperties">The default text run properties.</param>
        /// <returns>A list of shapeable text runs.</returns>
        private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice <char> text, TextRunProperties defaultProperties)
        {
            var defaultTypeface = defaultProperties.Typeface;

            var currentTypeface = defaultTypeface;

            if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count))
            {
                return(new ShapeableTextCharacters(text.Take(count),
                                                   new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
                                                                                defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)));
            }

            var codepoint = Codepoint.ReadAt(text, count, out _);

            //ToDo: Fix FontFamily fallback
            var matchFound =
                FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
                                                      defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface);

            if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
            {
                //Fallback found
                return(new ShapeableTextCharacters(text.Take(count),
                                                   new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
                                                                                defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)));
            }

            // no fallback found
            currentTypeface = defaultTypeface;

            var glyphTypeface = currentTypeface.GlyphTypeface;

            var enumerator = new GraphemeEnumerator(text);

            while (enumerator.MoveNext())
            {
                var grapheme = enumerator.Current;

                if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
                {
                    break;
                }

                count += grapheme.Text.Length;
            }

            return(new ShapeableTextCharacters(text.Take(count),
                                               new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
                                                                            defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)));
        }
Beispiel #5
0
        /// <summary>
        /// Creates a text style run with unique properties.
        /// </summary>
        /// <param name="text">The text to create text runs from.</param>
        /// <param name="defaultStyle"></param>
        /// <returns>A list of text runs.</returns>
        protected TextStyleRun CreateShapableTextStyleRun(ReadOnlySlice <char> text, TextStyle defaultStyle)
        {
            var defaultTypeface = defaultStyle.TextFormat.Typeface;

            var currentTypeface = defaultTypeface;

            if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count))
            {
                return(new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface,
                                                                                          defaultStyle.TextFormat.FontRenderingEmSize,
                                                                                          defaultStyle.Foreground, defaultStyle.TextDecorations)));
            }

            var codepoint = Codepoint.ReadAt(text, count, out _);

            //ToDo: Fix FontFamily fallback
            currentTypeface =
                FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style);

            if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
            {
                //Fallback found
                return(new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface,
                                                                                          defaultStyle.TextFormat.FontRenderingEmSize,
                                                                                          defaultStyle.Foreground, defaultStyle.TextDecorations)));
            }

            // no fallback found
            currentTypeface = defaultTypeface;

            var glyphTypeface = currentTypeface.GlyphTypeface;

            var enumerator = new GraphemeEnumerator(text);

            while (enumerator.MoveNext())
            {
                var grapheme = enumerator.Current;

                if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
                {
                    break;
                }

                count += grapheme.Text.Length;
            }

            return(new TextStyleRun(new TextPointer(text.Start, count),
                                    new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize,
                                                  defaultStyle.Foreground, defaultStyle.TextDecorations)));
        }
        private bool Run(GraphemeBreakData t)
        {
            var text     = Encoding.UTF32.GetString(MemoryMarshal.Cast <int, byte>(t.Codepoints).ToArray());
            var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast <int, byte>(t.Grapheme).ToArray()).AsSpan();

            var enumerator = new GraphemeEnumerator(text.AsMemory());

            enumerator.MoveNext();

            var actual = enumerator.Current.Text.Span;

            var pass = true;

            if (actual.Length != grapheme.Length)
            {
                pass = false;
            }

            if (pass)
            {
                for (int i = 0; i < grapheme.Length; i++)
                {
                    var a = grapheme[i];
                    var b = actual[i];

                    if (a != b)
                    {
                        pass = false;

                        break;
                    }
                }
            }

            if (!pass)
            {
                _outputHelper.WriteLine($"Failed line {t.LineNumber}");
                _outputHelper.WriteLine($"       Text: {text}");
                _outputHelper.WriteLine($" Codepoints: {string.Join(" ", t.Codepoints)}");
                _outputHelper.WriteLine($"   Grapheme: {string.Join(" ", t.Grapheme)}");
                _outputHelper.WriteLine($"       Line: {t.Line}");

                return(false);
            }


            return(true);
        }
        public void Should_Enumerate_Other()
        {
            const string text = "ABCDEFGHIJ";

            var enumerator = new GraphemeEnumerator(text.AsMemory());

            var count = 0;

            while (enumerator.MoveNext())
            {
                Assert.Equal(1, enumerator.Current.Text.Length);

                count++;
            }

            Assert.Equal(10, count);
        }
Beispiel #8
0
        private static int CoerceLength(ReadOnlySlice <char> text, int length)
        {
            var finalLength = 0;

            var graphemeEnumerator = new GraphemeEnumerator(text);

            while (graphemeEnumerator.MoveNext())
            {
                var grapheme = graphemeEnumerator.Current;

                finalLength += grapheme.Text.Length;

                if (finalLength >= length)
                {
                    return(finalLength);
                }
            }

            return(length);
        }
Beispiel #9
0
        /// <summary>
        /// Tries to get run properties.
        /// </summary>
        /// <param name="defaultTypeface"></param>
        /// <param name="text"></param>
        /// <param name="typeface">The typeface that is used to find matching characters.</param>
        /// <param name="count"></param>
        /// <returns></returns>
        protected bool TryGetRunProperties(ReadOnlySlice <char> text, Typeface typeface, Typeface defaultTypeface,
                                           out int count)
        {
            if (text.Length == 0)
            {
                count = 0;
                return(false);
            }

            var isFallback = typeface != defaultTypeface;

            count = 0;
            var script = Script.Common;
            //var direction = BiDiClass.LeftToRight;

            var font        = typeface.GlyphTypeface;
            var defaultFont = defaultTypeface.GlyphTypeface;

            var enumerator = new GraphemeEnumerator(text);

            while (enumerator.MoveNext())
            {
                var grapheme = enumerator.Current;

                var currentScript = grapheme.FirstCodepoint.Script;

                //var currentDirection = grapheme.FirstCodepoint.BiDiClass;

                //// ToDo: Implement BiDi algorithm
                //if (currentScript.HorizontalDirection != direction)
                //{
                //    if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
                //    {
                //        break;
                //    }
                //}

                if (currentScript != script)
                {
                    if (currentScript != Script.Inherited && currentScript != Script.Common)
                    {
                        if (script == Script.Inherited || script == Script.Common)
                        {
                            script = currentScript;
                        }
                        else
                        {
                            break;
                        }
                    }
                }

                if (isFallback)
                {
                    if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _))
                    {
                        break;
                    }
                }

                if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _))
                {
                    if (!grapheme.FirstCodepoint.IsWhiteSpace)
                    {
                        break;
                    }
                }

                count += grapheme.Text.Length;
            }

            return(count > 0);
        }
Beispiel #10
0
        public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied()
        {
            using (Start())
            {
                const string text = "אחד !\ntwo !\nשְׁלוֹשָׁה !";

                var red   = new SolidColorBrush(Colors.Red).ToImmutable();
                var black = Brushes.Black.ToImmutable();

                var expected = new TextLayout(
                    text,
                    Typeface.Default,
                    12.0f,
                    black,
                    textWrapping: TextWrapping.Wrap);

                var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast <ShapedTextCharacters>()
                                                                                .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();

                var outer = new GraphemeEnumerator(text.AsMemory());
                var inner = new GraphemeEnumerator(text.AsMemory());
                var i     = 0;
                var j     = 0;

                while (true)
                {
                    while (inner.MoveNext())
                    {
                        j += inner.Current.Text.Length;

                        if (j + i > text.Length)
                        {
                            break;
                        }

                        var spans = new[]
                        {
                            new ValueSpan <TextRunProperties>(i, j,
                                                              new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: red))
                        };

                        var actual = new TextLayout(
                            text,
                            Typeface.Default,
                            12.0f,
                            black,
                            textWrapping: TextWrapping.Wrap,
                            textStyleOverrides: spans);

                        var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast <ShapedTextCharacters>()
                                                                                    .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList();

                        Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);

                        for (var k = 0; k < expectedGlyphs.Count; k++)
                        {
                            Assert.Equal(expectedGlyphs[k], actualGlyphs[k]);
                        }
                    }

                    if (!outer.MoveNext())
                    {
                        break;
                    }

                    inner = new GraphemeEnumerator(text.AsMemory());

                    i += outer.Current.Text.Length;
                }
            }
        }
Beispiel #11
0
        /// <summary>
        /// Creates a shapeable text run with unique properties.
        /// </summary>
        /// <param name="text">The text to create text runs from.</param>
        /// <param name="defaultProperties">The default text run properties.</param>
        /// <param name="biDiLevel">The bidi level of the run.</param>
        /// <param name="previousProperties"></param>
        /// <returns>A list of shapeable text runs.</returns>
        private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice <char> text,
                                                                  TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties?previousProperties)
        {
            var defaultTypeface  = defaultProperties.Typeface;
            var currentTypeface  = defaultTypeface;
            var previousTypeface = previousProperties?.Typeface;

            if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
            {
                if (script == Script.Common && previousTypeface is not null)
                {
                    if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
                    {
                        return(new ShapeableTextCharacters(text.Take(fallbackCount),
                                                           defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel));
                    }
                }

                return(new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
                                                   biDiLevel));
            }

            if (previousTypeface is not null)
            {
                if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
                {
                    return(new ShapeableTextCharacters(text.Take(count),
                                                       defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel));
                }
            }

            var codepoint = Codepoint.ReplacementCodepoint;

            var codepointEnumerator = new CodepointEnumerator(text.Skip(count));

            while (codepointEnumerator.MoveNext())
            {
                if (codepointEnumerator.Current.IsWhiteSpace)
                {
                    continue;
                }

                codepoint = codepointEnumerator.Current;

                break;
            }

            //ToDo: Fix FontFamily fallback
            var matchFound =
                FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
                                                      defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
                                                      out currentTypeface);

            if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
            {
                //Fallback found
                return(new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
                                                   biDiLevel));
            }

            // no fallback found
            currentTypeface = defaultTypeface;

            var glyphTypeface = currentTypeface.GlyphTypeface;

            var enumerator = new GraphemeEnumerator(text);

            while (enumerator.MoveNext())
            {
                var grapheme = enumerator.Current;

                if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
                {
                    break;
                }

                count += grapheme.Text.Length;
            }

            return(new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel));
        }
Beispiel #12
0
        /// <summary>
        /// Tries to get run properties.
        /// </summary>
        /// <param name="defaultTypeface"></param>
        /// <param name="text"></param>
        /// <param name="typeface">The typeface that is used to find matching characters.</param>
        /// <param name="count"></param>
        /// <returns></returns>
        protected bool TryGetRunProperties(ReadOnlySlice <char> text, Typeface typeface, Typeface defaultTypeface,
                                           out int count)
        {
            if (text.Length == 0)
            {
                count = 0;
                return(false);
            }

            var isFallback = typeface != defaultTypeface;

            count = 0;
            var script    = Script.Unknown;
            var direction = BiDiClass.LeftToRight;

            var font        = typeface.GlyphTypeface;
            var defaultFont = defaultTypeface.GlyphTypeface;

            var enumerator = new GraphemeEnumerator(text);

            while (enumerator.MoveNext())
            {
                var currentGrapheme = enumerator.Current;

                var currentScript = currentGrapheme.FirstCodepoint.Script;

                var currentDirection = currentGrapheme.FirstCodepoint.BiDiClass;

                //// ToDo: Implement BiDi algorithm
                //if (currentScript.HorizontalDirection != direction)
                //{
                //    if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
                //    {
                //        break;
                //    }
                //}

                if (currentScript != script)
                {
                    if (script is Script.Unknown)
                    {
                        script = currentScript;
                    }
                    else
                    {
                        if (currentScript != Script.Inherited && currentScript != Script.Common)
                        {
                            break;
                        }
                    }
                }

                //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;
                    }
                }

                if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
                {
                    break;
                }

                if (direction == BiDiClass.RightToLeft && currentDirection == BiDiClass.CommonSeparator)
                {
                    break;
                }

                count    += currentGrapheme.Text.Length;
                direction = currentDirection;
            }

            return(count > 0);
        }