diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 3eb6d5b595..d50b051d9f 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -1,5 +1,7 @@ +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Media; namespace ControlCatalog.Pages { @@ -14,7 +16,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Find("fontComboBox"); - fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies; + fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e0cc9aa128..9084012619 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -297,7 +297,7 @@ namespace Avalonia.Controls.Presenters return new FormattedText { Text = "X", - Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), FontSize = FontSize, TextAlignment = TextAlignment, Constraint = availableSize, diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c7855ddfd1..8b8c7285be 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -352,7 +352,7 @@ namespace Avalonia.Controls return new FormattedText { Constraint = constraint, - Typeface = new Typeface(FontFamily, FontWeight, FontStyle), + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle), FontSize = FontSize, Text = text ?? string.Empty, TextAlignment = TextAlignment, diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index 771de524d9..a69a93e416 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -2,17 +2,17 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.Media.Fonts; namespace Avalonia.Media { public sealed class FontFamily { + public const string DefaultFontFamilyName = "$Default"; + static FontFamily() { - Default = new FontFamily(FontManager.Default.DefaultFontFamilyName); + Default = new FontFamily(DefaultFontFamilyName); } /// @@ -57,15 +57,6 @@ namespace Avalonia.Media /// public static FontFamily Default { get; } - /// - /// Represents all font families in the system. This can be an expensive call depending on platform implementation. - /// - /// - /// Consider using the new instead. - /// - public static IEnumerable SystemFontFamilies => - FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); - /// /// Gets the primary family name of the font family. /// @@ -86,10 +77,16 @@ namespace Avalonia.Media /// Gets the key for associated assets. /// /// - /// The family familyNames. + /// The family key. /// + /// Key is only used for custom fonts. public FontFamilyKey Key { get; } + /// + /// Returns True if this instance is the system's default. + /// + public bool IsDefault => Name.Equals(DefaultFontFamilyName); + /// /// Implicit conversion of string to FontFamily /// @@ -188,6 +185,21 @@ namespace Avalonia.Media } } + public static bool operator !=(FontFamily a, FontFamily b) + { + return !(a == b); + } + + public static bool operator ==(FontFamily a, FontFamily b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + public override bool Equals(object obj) { if (ReferenceEquals(this, obj)) diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index be1bd269ed..27ed9e64da 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -1,8 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using Avalonia.Media.Fonts; using Avalonia.Platform; namespace Avalonia.Media @@ -11,9 +13,53 @@ namespace Avalonia.Media /// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts. /// It is also responsible for the font fallback. /// - public abstract class FontManager + public sealed class FontManager { - public static readonly FontManager Default = CreateDefault(); + private readonly ConcurrentDictionary _typefaceCache = + new ConcurrentDictionary(); + private readonly FontFamily _defaultFontFamily; + + private FontManager(IFontManagerImpl platformImpl) + { + PlatformImpl = platformImpl; + + DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + + _defaultFontFamily = new FontFamily(DefaultFontFamilyName); + } + + public static FontManager Current + { + get + { + var current = AvaloniaLocator.Current.GetService(); + + if (current != null) + { + return current; + } + + var renderInterface = AvaloniaLocator.Current.GetService(); + + var fontManagerImpl = renderInterface?.CreateFontManager(); + + if (fontManagerImpl == null) + { + return null; + } + + current = new FontManager(fontManagerImpl); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); + + return current; + } + } + + /// + /// + /// + public IFontManagerImpl PlatformImpl { get; } /// /// Gets the system's default font family's name. @@ -21,25 +67,55 @@ namespace Avalonia.Media public string DefaultFontFamilyName { get; - protected set; } /// - /// Get all installed fonts in the system. + /// Get all installed fonts. /// If true the font collection is updated. /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); /// - /// Get a cached typeface from specified parameters. + /// Returns a new typeface, or an existing one if a matching typeface exists. /// /// The font family. /// The font weight. /// The font style. /// - /// The cached typeface. + /// The typeface. /// - public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); + public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal) + { + while (true) + { + if (fontFamily.IsDefault) + { + fontFamily = _defaultFontFamily; + } + + var key = new FontKey(fontFamily, fontWeight, fontStyle); + + if (_typefaceCache.TryGetValue(key, out var typeface)) + { + return typeface; + } + + typeface = new Typeface(fontFamily, fontWeight, fontStyle); + + if (_typefaceCache.TryAdd(key, typeface)) + { + return typeface; + } + + if (fontFamily == _defaultFontFamily) + { + return null; + } + + fontFamily = _defaultFontFamily; + } + } /// /// Tries to match a specified character to a typeface that supports specified font properties. @@ -53,60 +129,13 @@ namespace Avalonia.Media /// /// The matched typeface. /// - public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); - - public static FontManager CreateDefault() - { - var platformImpl = AvaloniaLocator.Current.GetService(); - - if (platformImpl != null) - { - return new PlatformFontManager(platformImpl); - } - - return new EmptyFontManager(); - } - - private class PlatformFontManager : FontManager - { - private readonly IFontManagerImpl _platformImpl; - - public PlatformFontManager(IFontManagerImpl platformImpl) - { - _platformImpl = platformImpl; - - DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - _platformImpl.GetInstalledFontFamilyNames(checkForUpdates); - - public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => - _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle); - - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => - _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture); - } - - private class EmptyFontManager : FontManager + public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + FontStyle fontStyle = FontStyle.Normal, + FontFamily fontFamily = null, CultureInfo culture = null) { - public EmptyFontManager() - { - DefaultFontFamilyName = "Empty"; - } - - public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => - new[] { DefaultFontFamilyName }; - - public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle); - - public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, - FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) => null; + return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : + null; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index eb0faf4187..8d2fd076c8 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -54,30 +54,67 @@ namespace Avalonia.Media.Fonts /// internal IReadOnlyList Names { get; } - /// /// - /// Returns an enumerator that iterates through the collection. + /// Returns an enumerator for the name collection. /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() + public Enumerator GetEnumerator() { - return Names.GetEnumerator(); + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } - /// - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly IReadOnlyList _names; + private int _pos; + + public Enumerator(IReadOnlyList names) + { + _names = names; + _pos = -1; + Current = default; + } + + public string Current + { + get; + private set; + } + + object IEnumerator.Current => Current; + + public void Dispose() { } + + public bool MoveNext() + { + if (_pos >= _names.Count - 1) + { + return false; + } + + Current = _names[++_pos]; + + return true; + + } + + public void Reset() + { + _pos = -1; + + Current = default; + } + } + /// /// Returns a that represents this instance. /// @@ -131,6 +168,21 @@ namespace Avalonia.Media.Fonts } } + public static bool operator !=(FamilyNameCollection a, FamilyNameCollection b) + { + return !(a == b); + } + + public static bool operator ==(FamilyNameCollection a, FamilyNameCollection b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + /// /// Determines whether the specified , is equal to this instance. /// diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs index 7733dd7d2a..887862face 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs @@ -58,6 +58,21 @@ namespace Avalonia.Media.Fonts } } + public static bool operator !=(FontFamilyKey a, FontFamilyKey b) + { + return !(a == b); + } + + public static bool operator ==(FontFamilyKey a, FontFamilyKey b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + return !(a is null) && a.Equals(b); + } + /// /// Determines whether the specified , is equal to this instance. /// diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs index 063fe8f20d..bed1fc6b83 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs @@ -10,13 +10,6 @@ namespace Avalonia.Media.Fonts { public static class FontFamilyLoader { - private static readonly IAssetLoader s_assetLoader; - - static FontFamilyLoader() - { - s_assetLoader = AvaloniaLocator.Current.GetService(); - } - /// /// Loads all font assets that belong to the specified /// @@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) { - var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var assetLoader = AvaloniaLocator.Current.GetService(); + + var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); var matchingAssets = availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf")); @@ -58,9 +53,11 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) { + var assetLoader = AvaloniaLocator.Current.GetService(); + var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location); - var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri); + var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri); string compareTo; diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs similarity index 65% rename from src/Skia/Avalonia.Skia/FontKey.cs rename to src/Avalonia.Visuals/Media/Fonts/FontKey.cs index bb3fe230c1..0ead585612 100644 --- a/src/Skia/Avalonia.Skia/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -2,24 +2,26 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Media; -namespace Avalonia.Skia +namespace Avalonia.Media.Fonts { - internal readonly struct FontKey : IEquatable + public readonly struct FontKey : IEquatable { + public readonly FontFamily FontFamily; public readonly FontStyle Style; public readonly FontWeight Weight; - public FontKey(FontWeight weight, FontStyle style) + public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style) { + FontFamily = fontFamily; Style = style; Weight = weight; } public override int GetHashCode() { - var hash = 17; + var hash = FontFamily.GetHashCode(); + hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Weight; @@ -33,7 +35,8 @@ namespace Avalonia.Skia public bool Equals(FontKey other) { - return Style == other.Style && + return FontFamily == other.FontFamily && + Style == other.Style && Weight == other.Weight; } } diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 3ba31f7e84..b03cf5908a 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -9,11 +9,9 @@ namespace Avalonia.Media { public sealed class GlyphTypeface : IDisposable { - private static readonly IPlatformRenderInterface s_platformRenderInterface = - AvaloniaLocator.Current.GetService(); - - public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface)) + public GlyphTypeface(Typeface typeface) { + PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface); } public GlyphTypeface(IGlyphTypefaceImpl platformImpl) diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index a6d5c8a43c..9a17bad7d2 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -13,8 +13,6 @@ namespace Avalonia.Media [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] public class Typeface : IEquatable { - public static readonly Typeface Default = new Typeface(FontFamily.Default); - private GlyphTypeface _glyphTypeface; /// @@ -50,6 +48,8 @@ namespace Avalonia.Media { } + public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + /// /// Gets the font family. /// diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 254b5d07d1..a8e6dcb29b 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; namespace Avalonia.Platform { @@ -12,7 +13,7 @@ namespace Avalonia.Platform /// /// Gets the system's default font family's name. /// - string DefaultFontFamilyName { get; } + string GetDefaultFontFamilyName(); /// /// Get all installed fonts in the system. @@ -20,17 +21,6 @@ namespace Avalonia.Platform /// IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - /// Get a typeface from specified parameters. - /// - /// The font family. - /// The font weight. - /// The font style. - /// - /// The typeface. - /// - Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - /// /// Tries to match a specified character to a typeface that supports specified font properties. /// @@ -39,10 +29,20 @@ namespace Avalonia.Platform /// The font style. /// The font family. This is optional and used for fallback lookup. /// The culture. + /// The matching font key. /// - /// The typeface. + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + + /// + /// Creates a glyph typeface. + /// + /// The typeface. + /// 0 + /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. /// - Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 5a0a7b2f19..edde10358c 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -113,12 +113,9 @@ namespace Avalonia.Platform IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride); /// - /// Creates a glyph typeface for specified typeface. + /// Creates a font manager implementation. /// - /// The typeface. - /// - /// The glyph typeface implementation. - /// - IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + /// The font manager. + IFontManagerImpl CreateFontManager(); } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index e341f02901..014101828f 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -7,7 +7,6 @@ namespace Avalonia.Rendering { public class RendererBase { - private static readonly Typeface s_fpsTypeface = new Typeface("Arial"); private static int s_fontSize = 18; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private int _framesThisSecond; @@ -19,7 +18,7 @@ namespace Avalonia.Rendering { _fpsText = new FormattedText { - Typeface = s_fpsTypeface, + Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), FontSize = s_fontSize }; } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 03de82178a..9cfa685191 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -1,9 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; @@ -13,13 +15,11 @@ namespace Avalonia.Skia { private SKFontManager _skFontManager = SKFontManager.Default; - public FontManagerImpl() + public string GetDefaultFontFamilyName() { - DefaultFontFamilyName = SKTypeface.Default.FamilyName; + return SKTypeface.Default.FamilyName; } - public string DefaultFontFamilyName { get; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { if (checkForUpdates) @@ -30,53 +30,86 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) - { - return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface; - } + [ThreadStatic] private static string[] s_languageTagBuffer; - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - var fontFamilyName = FontFamily.Default.Name; - if (culture == null) { culture = CultureInfo.CurrentUICulture; } + if (s_languageTagBuffer == null) + { + s_languageTagBuffer = new string[2]; + } + + s_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; + s_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; + if (fontFamily != null) { foreach (var familyName in fontFamily.FamilyNames) { var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, - (SKFontStyleSlant)fontStyle, - new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, s_languageTagBuffer, codepoint); if (skTypeface == null) { continue; } - fontFamilyName = familyName; + fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); - break; + return true; } } else { - var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, - (SKFontStyleSlant)fontStyle, - new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint); + var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, s_languageTagBuffer, codepoint); if (skTypeface != null) { - fontFamilyName = skTypeface.FamilyName; + fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); + + return true; + } + } + + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + var skTypeface = SKTypeface.Default; + + if (typeface.FontFamily.Key == null) + { + foreach (var familyName in typeface.FontFamily.FamilyNames) + { + skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + + if (skTypeface == SKTypeface.Default) + { + continue; + } + + break; } } + else + { + var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); + + skTypeface = fontCollection.Get(typeface.FontFamily, typeface.Weight, typeface.Style); + } - return GetTypeface(fontFamilyName, fontWeight, fontStyle); + return new GlyphTypefaceImpl(skTypeface); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index a9358cb458..8effb94ca9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -29,7 +29,7 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style); + var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl; _paint = new SKPaint { @@ -38,7 +38,7 @@ namespace Avalonia.Skia IsAntialias = true, LcdRenderText = true, SubpixelText = true, - Typeface = entry.SKTypeface, + Typeface = glyphTypeface.Typeface, TextSize = (float)fontSize, TextAlign = textAlignment.ToSKTextAlign() }; diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index e46f766255..d4dc70e808 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -14,9 +14,9 @@ namespace Avalonia.Skia { private bool _isDisposed; - public GlyphTypefaceImpl(Typeface typeface) + public GlyphTypefaceImpl(SKTypeface typeface) { - Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface; + Typeface = typeface; Face = new Face(GetTable) { @@ -81,7 +81,6 @@ namespace Avalonia.Skia /// public int LineGap { get; } - //ToDo: Get these values from HarfBuzz /// public int UnderlinePosition { get; } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ee0cfb2f06..e17d6fdce3 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using Avalonia.Controls.Platform.Surfaces; @@ -18,9 +17,6 @@ namespace Avalonia.Skia /// internal class PlatformRenderInterface : IPlatformRenderInterface { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } @@ -60,7 +56,7 @@ namespace Avalonia.Skia Size constraint, IReadOnlyList spans) { - return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans); + return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans); } public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); @@ -155,9 +151,10 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + /// + public IFontManagerImpl CreateFontManager() { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + return new FontManagerImpl(); } } } diff --git a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f6aabfae39 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")] diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 577567a8a1..d1c1961a8a 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -5,58 +5,59 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Avalonia.Media; +using Avalonia.Media.Fonts; +using SkiaSharp; namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry) + public void AddTypeface(FontKey key, SKTypeface typeface) { - if (!_fontFamilies.TryGetValue(familyName, out var fontFamily)) - { - fontFamily = new ConcurrentDictionary(); - - _fontFamilies.TryAdd(familyName, fontFamily); - } - - fontFamily.TryAdd(key, entry); + _typefaces.TryAdd(key, typeface); } - public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle) + public SKTypeface Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) { - var key = new FontKey(fontWeight, fontStyle); + var key = new FontKey(fontFamily, fontWeight, fontStyle); - return _fontFamilies.TryGetValue(familyName, out var fontFamily) ? - fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) : - new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default); + return GetNearestMatch(_typefaces, key); } - private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key) + private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) { - var keys = fontFamily.Keys.Where( + if (typefaces.ContainsKey(key)) + { + return typefaces[key]; + } + + var keys = typefaces.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( + keys = typefaces.Keys.Where( x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); if (!keys.Any()) { - keys = fontFamily.Keys.Where( + keys = typefaces.Keys.Where( x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); } } - key = keys.FirstOrDefault(); + if (keys.Length == 0) + { + return SKTypeface.Default; + } - fontFamily.TryGetValue(key, out var entry); + key = keys[0]; - return entry; + return typefaces[key]; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4bb42c7118..71edae26df 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -45,13 +45,11 @@ namespace Avalonia.Skia { var assetStream = assetLoader.Open(asset); - var skTypeface = SKTypeface.FromStream(assetStream); + var typeface = SKTypeface.FromStream(assetStream); - var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant); + var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); - var entry = new TypefaceCollectionEntry(typeface, skTypeface); - - typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry); + typeFaceCollection.AddTypeface(key, typeface); } return typeFaceCollection; diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index ce3aef755b..f16e967f42 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -25,11 +25,6 @@ namespace Avalonia.Skia AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); - - var fontManager = new FontManagerImpl(); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(fontManager); } /// diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs deleted file mode 100644 index 1c2b855032..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Concurrent; -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - /// - /// Cache for Skia typefaces. - /// - internal static class TypefaceCache - { - private static readonly ConcurrentDictionary> s_cache = - new ConcurrentDictionary>(); - - public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) - { - if (fontFamily.Key != null) - { - return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily) - .Get(fontFamily.Name, fontWeight, fontStyle); - } - - var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary()); - - var key = new FontKey(fontWeight, fontStyle); - - if (typefaceCollection.TryGetValue(key, out var entry)) - { - return entry; - } - - var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default; - - var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle); - - entry = new TypefaceCollectionEntry(typeface, skTypeface); - - typefaceCollection[key] = entry; - - return entry; - } - } -} diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs deleted file mode 100644 index ef9f889819..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Media; -using SkiaSharp; - -namespace Avalonia.Skia -{ - internal class TypefaceCollectionEntry - { - public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface) - { - Typeface = typeface; - SKTypeface = skTypeface; - } - public Typeface Typeface { get; } - public SKTypeface SKTypeface { get; } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 1bda5157a5..4068b31c9a 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -28,8 +28,6 @@ namespace Avalonia.Direct2D1 { public class Direct2D1Platform : IPlatformRenderInterface { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); private static readonly Direct2D1Platform s_instance = new Direct2D1Platform(); public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; } @@ -109,7 +107,6 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -194,9 +191,10 @@ namespace Avalonia.Direct2D1 return new WicBitmapImpl(format, data, size, dpi, stride); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + /// + public IFontManagerImpl CreateFontManager() { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + return new FontManagerImpl(); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index b455c4fbee..78bf25d607 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -24,10 +24,11 @@ namespace Avalonia.Direct2D1.Media { var fontFamily = typeface.FontFamily; var fontCollection = GetOrAddFontCollection(fontFamily); + int index; - foreach (var familyName in fontFamily.FamilyNames) + foreach (var name in fontFamily.FamilyNames) { - if (fontCollection.FindFamilyName(familyName, out var index)) + if (fontCollection.FindFamilyName(name, out index)) { return fontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, @@ -36,9 +37,9 @@ namespace Avalonia.Direct2D1.Media } } - InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i); + InstalledFontCollection.FindFamilyName("Segoe UI", out index); - return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont( + return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont( (FontWeight)typeface.Weight, FontStretch.Normal, (FontStyle)typeface.Style); diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 94de397652..31604ad15f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using Avalonia.Media; +using Avalonia.Media.Fonts; using Avalonia.Platform; using SharpDX.DirectWrite; using FontFamily = Avalonia.Media.FontFamily; @@ -14,14 +15,12 @@ namespace Avalonia.Direct2D1.Media { internal class FontManagerImpl : IFontManagerImpl { - public FontManagerImpl() + public string GetDefaultFontFamilyName() { //ToDo: Implement a real lookup of the system's default font. - DefaultFontFamilyName = "segoe ui"; + return "Segoe UI"; } - public string DefaultFontFamilyName { get; } - public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -36,17 +35,9 @@ namespace Avalonia.Direct2D1.Media return fontFamilies; } - public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { - //ToDo: Implement caching. - return new Typeface(fontFamily, fontWeight, fontStyle); - } - - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null) - { - var fontFamilyName = FontFamily.Default.Name; - var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; for (var i = 0; i < familyCount; i++) @@ -60,12 +51,21 @@ namespace Avalonia.Direct2D1.Media continue; } - fontFamilyName = font.FontFamily.FamilyNames.GetString(0); + var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - break; + fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); + + return true; } - return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle); + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return new GlyphTypefaceImpl(typeface); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b1a177ad24..949bf2be70 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var font = Direct2D1FontCollectionCache.GetFont(typeface)) + var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont; using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory, typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight, (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize)) diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs index a09d5c2d1c..26a8526c16 100644 --- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs @@ -2,9 +2,13 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using System.Runtime.CompilerServices; using Avalonia.Platform; using Avalonia.Direct2D1; [assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize), typeof(Direct2DChecker))] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")] +[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")] + diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs new file mode 100644 index 0000000000..82471915f4 --- /dev/null +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Reflection; +using Avalonia.Direct2D1.Media; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Direct2D1.UnitTests.Media +{ + public class FontManagerImplTests + { + private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"; + + [Fact] + public void Should_Create_Typeface_From_Fallback() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("A, B, Arial"))); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + + [Fact] + public void Should_Create_Typeface_For_Unknown_Font() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("Unknown"))); + + var font = glyphTypeface.DWFont; + + var defaultName = fontManager.GetDefaultFontFamilyName(); + + Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0)); + + Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight); + + Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); + } + } + + [Fact] + public void Should_Load_Typeface_From_Resource() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri))); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + } + } + + [Fact] + public void Should_Load_Nearest_Matching_Font() + { + using (AvaloniaLocator.EnterScope()) + { + Direct2D1Platform.Initialize(); + + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic)); + + var font = glyphTypeface.DWFont; + + Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0)); + } + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index a683e5cfca..f063d59ca4 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests It.IsAny>())) .Returns(new FormattedTextMock("TEST")); + renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl()); + var streamGeometry = new Mock(); streamGeometry.Setup(x => x.Open()) diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index bca34dd69d..73e63ae2ac 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -53,7 +53,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - new Typeface(fontFamily, fontWeight, fontStyle), + FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs new file mode 100644 index 0000000000..927f98b32b --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.UnitTests; +using SkiaSharp; +using Xunit; + +namespace Avalonia.Skia.UnitTests +{ + public class FontManagerImplTests + { + private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono"; + + [Fact] + public void Should_Create_Typeface_From_Fallback() + { + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName()))); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName); + + Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight); + + Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant); + } + + [Fact] + public void Should_Create_Typeface_For_Unknown_Font() + { + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily("Unknown"))); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName); + + Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight); + + Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant); + } + + [Fact] + public void Should_Load_Typeface_From_Resource() + { + using (AvaloniaLocator.EnterScope()) + { + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri))); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal("Noto Mono", skTypeface.FamilyName); + } + } + + [Fact] + public void Should_Load_Nearest_Matching_Font() + { + using (AvaloniaLocator.EnterScope()) + { + var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader"); + + var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader); + + var fontManager = new FontManagerImpl(); + + var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( + new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic)); + + var skTypeface = glyphTypeface.Typeface; + + Assert.Equal("Noto Mono", skTypeface.FamilyName); + } + } + } +} diff --git a/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf new file mode 100644 index 0000000000..3560a3a0c8 Binary files /dev/null and b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf differ diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 272b1fc489..b1d89037da 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -7,6 +7,9 @@ false latest + + + @@ -20,7 +23,7 @@ + - diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs new file mode 100644 index 0000000000..faf6f98138 --- /dev/null +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using Moq; + +namespace Avalonia.UnitTests +{ + public class MockFontManagerImpl : IFontManagerImpl + { + public string GetDefaultFontFamilyName() + { + return "Default"; + } + + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) + { + return new[] { "Default" }; + } + + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, + CultureInfo culture, out FontKey fontKey) + { + fontKey = default; + + return false; + } + + public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + { + return Mock.Of(); + } + } +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 187853283f..ba436405ce 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -79,9 +79,9 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + public IFontManagerImpl CreateFontManager() { - return Mock.Of(); + return new MockFontManagerImpl(); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs new file mode 100644 index 0000000000..6cbab08905 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -0,0 +1,25 @@ +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class FontManagerTests + { + [Fact] + public void Should_Create_Single_Instance_Typeface() + { + using (AvaloniaLocator.EnterScope()) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new MockPlatformRenderInterface()); + + var fontFamily = new FontFamily("MyFont"); + + var typeface = FontManager.Current.GetOrAddTypeface(fontFamily); + + Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 032b6582a9..5cd313b169 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -8,8 +8,6 @@ namespace Avalonia.Visuals.UnitTests.VisualTree { class MockRenderInterface : IPlatformRenderInterface { - public IEnumerable InstalledFontNames => new string[0]; - public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, @@ -57,6 +55,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IFontManagerImpl CreateFontManager() + { + throw new NotImplementedException(); + } + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt) { throw new NotImplementedException();