Browse Source

Reworked the font manager to better manage resoures and also work properly with the application lifetime

pull/3234/head
Benedikt Schroeder 7 years ago
parent
commit
1b0221e224
  1. 4
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  2. 2
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  3. 2
      src/Avalonia.Controls/TextBlock.cs
  4. 38
      src/Avalonia.Visuals/Media/FontFamily.cs
  5. 151
      src/Avalonia.Visuals/Media/FontManager.cs
  6. 80
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  7. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
  8. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
  9. 15
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  10. 6
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  11. 4
      src/Avalonia.Visuals/Media/Typeface.cs
  12. 30
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  13. 9
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  14. 3
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  15. 77
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  16. 4
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  17. 5
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  18. 11
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  19. 4
      src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
  20. 47
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  21. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  22. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  23. 47
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  24. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  25. 8
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  26. 9
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  27. 34
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  28. 2
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  29. 4
      src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
  30. 111
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  31. 2
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  32. 2
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  33. 93
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  34. BIN
      tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf
  35. 5
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  36. 35
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  37. 4
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  38. 25
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  39. 7
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

4
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<ComboBox>("fontComboBox");
fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies;
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.SelectedIndex = 0;
}
}

2
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,

2
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,

38
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);
}
/// <inheritdoc />
@ -57,15 +57,6 @@ namespace Avalonia.Media
/// </summary>
public static FontFamily Default { get; }
/// <summary>
/// Represents all font families in the system. This can be an expensive call depending on platform implementation.
/// </summary>
/// <remarks>
/// Consider using the new <see cref="FontManager"/> instead.
/// </remarks>
public static IEnumerable<FontFamily> SystemFontFamilies =>
FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
/// <summary>
/// Gets the primary family name of the font family.
/// </summary>
@ -86,10 +77,16 @@ namespace Avalonia.Media
/// Gets the key for associated assets.
/// </summary>
/// <value>
/// The family familyNames.
/// The family key.
/// </value>
/// <remarks>Key is only used for custom fonts.</remarks>
public FontFamilyKey Key { get; }
/// <summary>
/// Returns <c>True</c> if this instance is the system's default.
/// </summary>
public bool IsDefault => Name.Equals(DefaultFontFamilyName);
/// <summary>
/// Implicit conversion of string to FontFamily
/// </summary>
@ -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))

151
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.
/// </summary>
public abstract class FontManager
public sealed class FontManager
{
public static readonly FontManager Default = CreateDefault();
private readonly ConcurrentDictionary<FontKey, Typeface> _typefaceCache =
new ConcurrentDictionary<FontKey, Typeface>();
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<FontManager>();
if (current != null)
{
return current;
}
var renderInterface = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var fontManagerImpl = renderInterface?.CreateFontManager();
if (fontManagerImpl == null)
{
return null;
}
current = new FontManager(fontManagerImpl);
AvaloniaLocator.CurrentMutable.Bind<FontManager>().ToConstant(current);
return current;
}
}
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Gets the system's default font family's name.
@ -21,25 +67,55 @@ namespace Avalonia.Media
public string DefaultFontFamilyName
{
get;
protected set;
}
/// <summary>
/// Get all installed fonts in the system.
/// Get all installed fonts.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
public abstract IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
/// <summary>
/// Get a cached typeface from specified parameters.
/// Returns a new typeface, or an existing one if a matching typeface exists.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The cached typeface.
/// The typeface.
/// </returns>
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;
}
}
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
@ -53,60 +129,13 @@ namespace Avalonia.Media
/// <returns>
/// The matched typeface.
/// </returns>
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<IFontManagerImpl>();
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<string> 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<string> 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;
}
}
}

80
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@ -54,30 +54,67 @@ namespace Avalonia.Media.Fonts
/// </value>
internal IReadOnlyList<string> Names { get; }
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// Returns an enumerator for the name collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<string> GetEnumerator()
public Enumerator GetEnumerator()
{
return Names.GetEnumerator();
return new Enumerator(this);
}
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public struct Enumerator : IEnumerator, IEnumerator<string>
{
private readonly IReadOnlyList<string> _names;
private int _pos;
public Enumerator(IReadOnlyList<string> 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;
}
}
/// <summary>
/// Returns a <see cref="string" /> that represents this instance.
/// </summary>
@ -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);
}
/// <summary>
/// Determines whether the specified <see cref="object" />, is equal to this instance.
/// </summary>

15
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);
}
/// <summary>
/// Determines whether the specified <see cref="object" />, is equal to this instance.
/// </summary>

15
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<IAssetLoader>();
}
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary>
@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
{
var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
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
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
{
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
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;

15
src/Skia/Avalonia.Skia/FontKey.cs → 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<FontKey>
public readonly struct FontKey : IEquatable<FontKey>
{
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;
}
}

6
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<IPlatformRenderInterface>();
public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface))
public GlyphTypeface(Typeface typeface)
{
PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface);
}
public GlyphTypeface(IGlyphTypefaceImpl platformImpl)

4
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<Typeface>
{
public static readonly Typeface Default = new Typeface(FontFamily.Default);
private GlyphTypeface _glyphTypeface;
/// <summary>
@ -50,6 +48,8 @@ namespace Avalonia.Media
{
}
public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default);
/// <summary>
/// Gets the font family.
/// </summary>

30
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
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
string DefaultFontFamilyName { get; }
string GetDefaultFontFamilyName();
/// <summary>
/// Get all installed fonts in the system.
@ -20,17 +21,6 @@ namespace Avalonia.Platform
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Get a typeface from specified parameters.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The typeface.
/// </returns>
Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
/// </summary>
@ -39,10 +29,20 @@ namespace Avalonia.Platform
/// <param name="fontStyle">The font style.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param>
/// <param name="fontKey">The matching font key.</param>
/// <returns>
/// The typeface.
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey);
/// <summary>
/// Creates a glyph typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>0
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
/// </returns>
Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
}
}

9
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);
/// <summary>
/// Creates a glyph typeface for specified typeface.
/// Creates a font manager implementation.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>
/// The glyph typeface implementation.
/// </returns>
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
/// <returns>The font manager.</returns>
IFontManagerImpl CreateFontManager();
}
}

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

77
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<string> 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);
}
}
}

4
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()
};

5
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
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
//ToDo: Get these values from HarfBuzz
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }

11
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
/// </summary>
internal class PlatformRenderInterface : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; }
@ -60,7 +56,7 @@ namespace Avalonia.Skia
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> 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)
/// <inheritdoc />
public IFontManagerImpl CreateFontManager()
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
return new FontManagerImpl();
}
}
}

4
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")]

47
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<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> _fontFamilies =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
private readonly ConcurrentDictionary<FontKey, SKTypeface> _typefaces =
new ConcurrentDictionary<FontKey, SKTypeface>();
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<FontKey, TypefaceCollectionEntry>();
_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<FontKey, TypefaceCollectionEntry> fontFamily, FontKey key)
private static SKTypeface GetNearestMatch(IDictionary<FontKey, SKTypeface> 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];
}
}
}

8
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;

5
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@ -25,11 +25,6 @@ namespace Avalonia.Skia
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
var fontManager = new FontManagerImpl();
AvaloniaLocator.CurrentMutable
.Bind<IFontManagerImpl>().ToConstant(fontManager);
}
/// <summary>

47
src/Skia/Avalonia.Skia/TypefaceCache.cs

@ -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
{
/// <summary>
/// Cache for Skia typefaces.
/// </summary>
internal static class TypefaceCache
{
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> s_cache =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
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<FontKey, TypefaceCollectionEntry>());
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;
}
}
}

19
src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs

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

8
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -28,8 +28,6 @@ namespace Avalonia.Direct2D1
{
public class Direct2D1Platform : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
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<IPlatformRenderInterface>().ToConstant(s_instance);
AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().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)
/// <inheritdoc />
public IFontManagerImpl CreateFontManager()
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
return new FontManagerImpl();
}
}
}

9
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);

34
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<string> 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);
}
}
}

2
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))

4
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")]

111
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<IAssetLoader>().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<IAssetLoader>().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));
}
}
}
}

2
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
.Returns(new FormattedTextMock("TEST"));
renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl());
var streamGeometry = new Mock<IStreamGeometryImpl>();
streamGeometry.Setup(x =>
x.Open())

2
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@ -53,7 +53,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text,
new Typeface(fontFamily, fontWeight, fontStyle),
FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle),
fontSize,
textAlignment,
wrapping,

93
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<IAssetLoader>().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<IAssetLoader>().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);
}
}
}
}

BIN
tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf

Binary file not shown.

5
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@ -7,6 +7,9 @@
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.UnitTests\Assets\*.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
@ -20,7 +23,7 @@
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
</ItemGroup>
<Import Project="..\..\src\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\src\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
</Project>

35
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<string> 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<IGlyphTypefaceImpl>();
}
}
}

4
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<IGlyphTypefaceImpl>();
return new MockFontManagerImpl();
}
}
}

25
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<IPlatformRenderInterface>().ToConstant(new MockPlatformRenderInterface());
var fontFamily = new FontFamily("MyFont");
var typeface = FontManager.Current.GetOrAddTypeface(fontFamily);
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
}
}
}
}

7
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -8,8 +8,6 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
{
class MockRenderInterface : IPlatformRenderInterface
{
public IEnumerable<string> 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();

Loading…
Cancel
Save