From 3fdfdd64d2350db0888aed168b439a3b0ad827b9 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 28 Jan 2022 21:48:45 +0100 Subject: [PATCH] Fix font fallback Fix TextPresenter measure --- .../Presenters/TextPresenter.cs | 4 +- src/Avalonia.Visuals/Media/GlyphRun.cs | 10 +++ src/Avalonia.Visuals/Media/GlyphTypeface.cs | 2 - .../TextFormatting/ShapedTextCharacters.cs | 9 +++ .../Media/TextFormatting/TextCharacters.cs | 76 +++++++++---------- .../Media/TextFormatting/TextFormatterImpl.cs | 7 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 2 + .../TextFormatting/TextFormatterTests.cs | 32 ++++++++ 8 files changed, 98 insertions(+), 44 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 3dcaff5171..a0558df23c 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -515,9 +515,11 @@ namespace Avalonia.Controls.Presenters protected override Size MeasureOverride(Size availableSize) { - if (availableSize != Size.Infinity) + if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint) { _constraint = availableSize; + + InvalidateTextLayout(); } return TextLayout.Size; diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index dfefa98f50..ef5ffb8d78 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -669,6 +669,15 @@ namespace Avalonia.Media var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster; + if (codepointIndex < 0) + { + trailingWhitespaceLength = _characters.Length; + + glyphCount = GlyphClusters.Count; + + break; + } + var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _); if (!codepoint.IsWhiteSpace) @@ -682,6 +691,7 @@ namespace Avalonia.Media } trailingWhitespaceLength++; + glyphCount++; } } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 67dfbb84b6..45ef04e77f 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -5,8 +5,6 @@ namespace Avalonia.Media { public sealed class GlyphTypeface : IDisposable { - public const int InvisibleGlyph = 3; - public GlyphTypeface(Typeface typeface) : this(FontManager.Current.PlatformImpl.CreateGlyphTypeface(typeface)) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index d5af819c39..96b3857098 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -152,6 +152,15 @@ namespace Avalonia.Media.TextFormatting var first = new ShapedTextCharacters(splitBuffer.First, Properties); + #if DEBUG + + if (first.Text.Length != length) + { + throw new InvalidOperationException("Split length mismatch."); + } + + #endif + var second = new ShapedTextCharacters(splitBuffer.Second!, Properties); return new SplitResult(first, second); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index 9116b58695..494cbcb890 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -37,17 +38,20 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel) + internal IList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, + ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(2); while (!runText.IsEmpty) { - var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel); + var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties); shapeableCharacters.Add(shapeableRun); runText = runText.Skip(shapeableRun.Text.Length); + + previousProperties = shapeableRun.Properties; } return shapeableCharacters; @@ -59,15 +63,29 @@ namespace Avalonia.Media.TextFormatting /// The text to create text runs from. /// The default text run properties. /// The bidi level of the run. + /// /// A list of shapeable text runs. - private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties, sbyte biDiLevel) + private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, + TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; - if (TryGetShapeableLength(text, currentTypeface, defaultTypeface, out var count)) + if (TryGetShapeableLength(text, currentTypeface, out var count, out var script)) { + var previousTypeface = previousProperties?.Typeface; + + if (script == Script.Common && previousTypeface is not null) + { + if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _)) + { + return new ShapeableTextCharacters(text.Take(fallbackCount), + new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize, + defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel); + } + } + return new ShapeableTextCharacters(text.Take(count), new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel); @@ -94,7 +112,7 @@ namespace Avalonia.Media.TextFormatting FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); - if (matchFound && TextCharacters.TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count)) + if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _)) { //Fallback found return new ShapeableTextCharacters(text.Take(count), @@ -127,30 +145,26 @@ namespace Avalonia.Media.TextFormatting } /// - /// Tries to get run properties. + /// Tries to get a shapeable length that is supported by the specified typeface. /// - /// - /// + /// The text. /// The typeface that is used to find matching characters. - /// + /// The shapeable length. + /// /// - protected static bool TryGetShapeableLength(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface, - out int length) + protected static bool TryGetShapeableLength(ReadOnlySlice text, Typeface typeface, out int length, + out Script script) { + length = 0; + script = Script.Unknown; + if (text.Length == 0) { - length = 0; return false; } - var isFallback = typeface != defaultTypeface; - - length = 0; - var script = Script.Unknown; - var font = typeface.GlyphTypeface; - var defaultFont = defaultTypeface.GlyphTypeface; - + var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) @@ -161,7 +175,8 @@ namespace Avalonia.Media.TextFormatting if (currentScript != script) { - if (script is Script.Unknown || currentScript != Script.Common && (script is Script.Common || script is Script.Inherited)) + if (script is Script.Unknown || currentScript != Script.Common && + (script is Script.Common || script is Script.Inherited)) { script = currentScript; } @@ -174,23 +189,8 @@ namespace Avalonia.Media.TextFormatting } } - //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 _)) + //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 9a41d01b56..101f273798 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -208,6 +208,7 @@ namespace Avalonia.Media.TextFormatting var levelIndex = 0; var runLevel = levels[0]; + TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; var runText = ReadOnlySlice.Empty; @@ -231,7 +232,7 @@ namespace Avalonia.Media.TextFormatting if (j == runText.Length) { - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel); + yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); runLevel = levels[levelIndex]; @@ -244,7 +245,7 @@ namespace Avalonia.Media.TextFormatting } // End of this run - yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel); + yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties); runText = runText.Skip(j); @@ -260,7 +261,7 @@ namespace Avalonia.Media.TextFormatting yield break; } - yield return currentRun.GetShapeableCharacters(runText, runLevel); + yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties); } /// diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 9601fece25..5b6e5af60f 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -64,6 +64,8 @@ namespace Avalonia.Skia public SKTypeface Typeface { get; } + public int ReplacementCodepoint { get; } + /// public short DesignEmHeight { get; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 10a05f98d1..326997328b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -461,6 +461,38 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(expectedOffset, textLine.Start); } } + + [Fact] + public void Should_Wrap_Syriac() + { + using (Start()) + { + const string text = + "܀ ܁ ܂ ܃ ܄ ܅ ܆ ܇ ܈ ܉ ܊ ܋ ܌ ܍ ܏ ܐ ܑ ܒ ܓ ܔ ܕ ܖ ܗ ܘ ܙ ܚ ܛ ܜ ܝ ܞ ܟ ܠ ܡ ܢ ܣ ܤ ܥ ܦ ܧ ܨ ܩ ܪ ܫ ܬ ܰ ܱ ܲ ܳ ܴ ܵ ܶ ܷ ܸ ܹ ܺ ܻ ܼ ܽ ܾ ܿ ݀ ݁ ݂ ݃ ݄ ݅ ݆ ݇ ݈ ݉ ݊"; + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var paragraphProperties = + new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + var formatter = new TextFormatterImpl(); + + var textPosition = 87; + TextLineBreak lastBreak = null; + + while (textPosition < text.Length) + { + var textLine = + formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak); + + Assert.Equal(textLine.TextRange.Length, textLine.TextRuns.Sum(x => x.TextSourceLength)); + + textPosition += textLine.TextRange.Length; + + lastBreak = textLine.TextLineBreak; + } + } + } [Fact] public void Should_FormatLine_With_Emergency_Breaks()