diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index e82d5b7ba5..2dabb29e76 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -132,7 +132,7 @@ namespace Avalonia.Media { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - var glyphTypeface = typeface.GlyphTypeface; + var glyphTypeface = GetOrAddGlyphTypeface(typeface); if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ return true; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index d444a58297..ac196bf7e0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting { ShapedBuffer = shapedBuffer; Properties = properties; - TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); + TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); } public bool IsReversed { get; private set; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 3ccfb40c4a..94db739d4d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -69,6 +69,7 @@ namespace Avalonia.Media.TextFormatting /// The characters to create text runs from. /// The default text run properties. /// The bidi level of the run. + /// The font manager to use. /// /// A list of shapeable text runs. private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, @@ -76,31 +77,32 @@ namespace Avalonia.Media.TextFormatting ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; - var currentTypeface = defaultTypeface; + var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface; var previousTypeface = previousProperties?.Typeface; + var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script)) { - if (script == Script.Common && previousTypeface is not null) + if (script == Script.Common && previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _)) { return new UnshapedTextRun(text.Slice(0, fallbackCount), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); + defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); } } - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); } - if (previousTypeface is not null) + if (previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _)) { return new UnshapedTextRun(text.Slice(0, count), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); + defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); } } @@ -124,25 +126,23 @@ namespace Avalonia.Media.TextFormatting var matchFound = fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, - out currentTypeface); + out var fallbackTypeface); - if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) + var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); + + if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _)) { //Fallback found - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), biDiLevel); } // no fallback found - currentTypeface = defaultTypeface; - - var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(textSpan); while (enumerator.MoveNext(out var grapheme)) { - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } @@ -157,15 +157,15 @@ namespace Avalonia.Media.TextFormatting /// Tries to get a shapeable length that is supported by the specified typeface. /// /// The characters to shape. - /// The typeface that is used to find matching characters. - /// + /// The typeface that is used to find matching characters. + /// The default typeface. /// The shapeable length. /// /// internal static bool TryGetShapeableLength( ReadOnlySpan text, - Typeface typeface, - Typeface? defaultTypeface, + IGlyphTypeface glyphTypeface, + IGlyphTypeface? defaultGlyphTypeface, out int length, out Script script) { @@ -177,22 +177,22 @@ namespace Avalonia.Media.TextFormatting return false; } - var font = typeface.GlyphTypeface; - var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext(out var currentGrapheme)) { - var currentScript = currentGrapheme.FirstCodepoint.Script; + var currentCodepoint = currentGrapheme.FirstCodepoint; + var currentScript = currentCodepoint.Script; - if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsWhiteSpace + && defaultGlyphTypeface != null + && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } //Stop at the first missing glyph - if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index b0242be87e..bc19690196 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -27,6 +27,7 @@ namespace Avalonia.Media.TextFormatting TextLineBreak? nextLineBreak = null; IReadOnlyList? textRuns; var objectPool = FormattingObjectPool.Instance; + var fontManager = FontManager.Current; var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); @@ -42,7 +43,7 @@ namespace Avalonia.Media.TextFormatting } else { - shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection); textRuns = shapedTextRuns; if (nextLineBreak == null && textEndOfLine != null) @@ -72,7 +73,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.Wrap: { textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); break; } default: @@ -178,12 +179,13 @@ namespace Avalonia.Media.TextFormatting /// The default paragraph properties. /// The resolved flow direction. /// A pool used to get reusable formatting objects. + /// The font manager to use. /// /// A list of shaped text characters. /// private static RentedList ShapeTextRuns(IReadOnlyList textRuns, TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, - out FlowDirection resolvedFlowDirection) + FontManager fontManager, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; var shapedRuns = objectPool.TextRunLists.Rent(); @@ -223,7 +225,7 @@ namespace Avalonia.Media.TextFormatting var processedRuns = objectPool.TextRunLists.Rent(); - CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns); bidiData.Reset(); bidiAlgorithm.Reset(); @@ -240,7 +242,9 @@ namespace Avalonia.Media.TextFormatting { groupedRuns.Clear(); groupedRuns.Add(shapeableRun); + var text = shapeableRun.Text; + var properties = shapeableRun.Properties; while (index + 1 < processedRuns.Count) { @@ -251,7 +255,7 @@ namespace Avalonia.Media.TextFormatting if (shapeableRun.BidiLevel == nextRun.BidiLevel && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) - && CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) + && CanShapeTogether(properties, nextRun.Properties)) { groupedRuns.Add(nextRun); index++; @@ -263,10 +267,10 @@ namespace Avalonia.Media.TextFormatting break; } - var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, - currentRun.Properties.FontRenderingEmSize, - shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, - paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); + var shaperOptions = new TextShaperOptions( + properties.CachedGlyphTypeface, + properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, + paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); @@ -377,10 +381,11 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. + /// The font manager to use. /// A list that will be filled with the processed runs. /// private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, - RentedList processedRuns) + FontManager fontManager, RentedList processedRuns) { if (levels.Length == 0) { @@ -393,7 +398,6 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; ReadOnlyMemory runText = default; - var fontManager = FontManager.Current; for (var i = 0; i < textCharacters.Count; i++) { @@ -638,11 +642,11 @@ namespace Avalonia.Media.TextFormatting /// /// The empty text line. public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) + TextParagraphProperties paragraphProperties, FontManager fontManager) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; - var glyphTypeface = properties.Typeface.GlyphTypeface; + var glyphTypeface = properties.CachedGlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; @@ -666,14 +670,15 @@ namespace Avalonia.Media.TextFormatting /// /// The current line break if the line was explicitly broken. /// A pool used to get reusable formatting objects. + /// The font manager to use. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) + TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -869,7 +874,7 @@ namespace Avalonia.Media.TextFormatting { var textShaper = TextShaper.Current; - var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface; + var glyphTypeface = textRun.Properties!.CachedGlyphTypeface; var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 7a74dc89ae..bb58e0d692 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -427,11 +427,12 @@ namespace Avalonia.Media.TextFormatting private TextLine[] CreateTextLines() { var objectPool = FormattingObjectPool.Instance; + var fontManager = FontManager.Current; if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, - FormattingObjectPool.Instance); + fontManager); Bounds = new Rect(0, 0, 0, textLine.Height); @@ -458,7 +459,7 @@ namespace Avalonia.Media.TextFormatting if (previousLine != null && previousLine.NewLineLength > 0) { var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, - _paragraphProperties, objectPool); + _paragraphProperties, fontManager); textLines.Add(emptyTextLine); @@ -517,7 +518,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager); textLines.Add(textLine); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 260fcaccbe..ad3244a3a5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1256,7 +1256,7 @@ namespace Avalonia.Media.TextFormatting private TextLineMetrics CreateLineMetrics() { - var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics; + var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; @@ -1285,12 +1285,13 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextRun textRun: { + var properties = textRun.Properties; var textMetrics = - new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); + new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); - if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) + if (fontRenderingEmSize < properties.FontRenderingEmSize) { - fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; + fontRenderingEmSize = properties.FontRenderingEmSize; if (ascent > textMetrics.Ascent) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 7bad99f33f..1622bc3b6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting /// public abstract class TextRunProperties : IEquatable { + private IGlyphTypeface? _cachedGlyphTypeFace; + /// /// Run typeface /// @@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting /// public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline; + internal IGlyphTypeface CachedGlyphTypeface + => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface; + public bool Equals(TextRunProperties? other) { if (ReferenceEquals(null, other))