diff --git a/api/Avalonia.Win32.Interoperability.nupkg.xml b/api/Avalonia.Win32.Interoperability.nupkg.xml
new file mode 100644
index 0000000000..3672bb9b99
--- /dev/null
+++ b/api/Avalonia.Win32.Interoperability.nupkg.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ CP0002
+ M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@)
+ baseline/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll
+ current/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll
+
+
+ CP0002
+ M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@)
+ baseline/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll
+ current/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll
+
+
+ CP0002
+ M:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost.PreFilterMessage(System.Windows.Forms.Message@)
+ baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll
+ current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll
+
+
+ CP0008
+ T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost
+ baseline/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll
+ current/Avalonia.Win32.Interoperability/lib/net461/Avalonia.Win32.Interoperability.dll
+
+
+ CP0008
+ T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost
+ baseline/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll
+ current/Avalonia.Win32.Interoperability/lib/net6.0-windows7.0/Avalonia.Win32.Interoperability.dll
+
+
+ CP0008
+ T:Avalonia.Win32.Interoperability.WinFormsAvaloniaControlHost
+ baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll
+ current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll
+
+
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index f5864e85cf..af46d2fb2e 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -1,24 +1,150 @@
+
+ CP0001
+ T:Avalonia.Media.Fonts.FontFamilyLoader
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0001
+ T:Avalonia.Media.Fonts.FontFamilyLoader
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0001
+ T:Avalonia.Media.Fonts.FontFamilyLoader
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0001
+ T:Avalonia.Media.Fonts.FontFamilyLoader
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0002
+ F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)
baseline/Avalonia/lib/net6.0/Avalonia.Dialogs.dll
current/Avalonia/lib/net6.0/Avalonia.Dialogs.dll
+
+ CP0002
+ F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)
baseline/Avalonia/lib/net8.0/Avalonia.Dialogs.dll
current/Avalonia/lib/net8.0/Avalonia.Dialogs.dll
+
+ CP0002
+ F:Avalonia.Media.Fonts.FontCollectionBase._glyphTypefaceCache
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.FontCollectionBase.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Media.Fonts.IFontCollection.Initialize(Avalonia.Platform.IFontManagerImpl)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Dialogs.Internal.ManagedFileChooserFilterViewModel.#ctor(Avalonia.Platform.Storage.FilePickerFileType)
baseline/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll
current/Avalonia/lib/netstandard2.0/Avalonia.Dialogs.dll
+
+ CP0006
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)
@@ -43,6 +169,12 @@
baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+ CP0006
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)
@@ -97,6 +229,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0006
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)
@@ -151,6 +289,12 @@
baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+ CP0006
+ M:Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32,Avalonia.Media.FontStyle,Avalonia.Media.FontWeight,Avalonia.Media.FontStretch,System.String,System.Globalization.CultureInfo,Avalonia.Media.Typeface@)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Platform.IPlatformRenderInterfaceImportedImage.SnapshotWithTimelineSemaphores(Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64,Avalonia.Platform.IPlatformRenderInterfaceImportedSemaphore,System.UInt64)
@@ -181,4 +325,124 @@
baseline/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll
current/Avalonia/lib/netstandard2.0/Avalonia.OpenGL.dll
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Count
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Count
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Count
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Count
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Count
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Count
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Count
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.get_Item(System.Int32)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0012
+ M:Avalonia.Media.Fonts.FontCollectionBase.GetEnumerator
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Count
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0012
+ P:Avalonia.Media.Fonts.FontCollectionBase.Item(System.Int32)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs
index 5a49511a5a..595a926f0c 100644
--- a/src/Avalonia.Base/Media/FontManager.cs
+++ b/src/Avalonia.Base/Media/FontManager.cs
@@ -31,8 +31,6 @@ namespace Avalonia.Media
{
PlatformImpl = platformImpl;
- AddFontCollection(new SystemFontCollection(this));
-
var options = AvaloniaLocator.Current.GetService();
_fontFallbacks = options?.FontFallbacks;
_fontFamilyMappings = options?.FontFamilyMappings;
@@ -76,7 +74,19 @@ namespace Avalonia.Media
///
/// Get all system fonts.
///
- public IFontCollection SystemFonts => _fontCollections[SystemFontsKey];
+ public IFontCollection SystemFonts
+ {
+ get
+ {
+ if (TryGetFontCollection(SystemFontsKey, out var fontCollection))
+ {
+ return fontCollection;
+ }
+
+ // Fallback to an empty system font collection
+ return new EmptySystemFontCollection();
+ }
+ }
internal IFontManagerImpl PlatformImpl { get; }
@@ -93,12 +103,13 @@ namespace Avalonia.Media
glyphTypeface = null;
var fontFamily = GetMappedFontFamily(typeface.FontFamily);
-
+
if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
{
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
+
if (fontFamily.Key != null)
{
if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
@@ -167,7 +178,7 @@ namespace Avalonia.Media
FontFamily GetMappedFontFamily(FontFamily fontFamily)
{
- if (_fontFamilyMappings == null ||!_fontFamilyMappings.TryGetValue(fontFamily.FamilyNames.PrimaryFamilyName, out var mappedFontFamily))
+ if (_fontFamilyMappings == null || !_fontFamilyMappings.TryGetValue(fontFamily.FamilyNames.PrimaryFamilyName, out var mappedFontFamily))
{
return fontFamily;
}
@@ -222,8 +233,6 @@ namespace Avalonia.Media
return fontCollection;
});
-
- fontCollection.Initialize(PlatformImpl);
}
///
@@ -288,7 +297,7 @@ namespace Avalonia.Media
if (TryGetFontCollection(source, out var fontCollection) &&
// With composite fonts we need to first check if the font collection contains the family if not we skip it
- fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) &&
+ fontCollection.TryGetGlyphTypeface(familyName, fontStyle, fontWeight, fontStretch, out _) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
return true;
@@ -319,7 +328,7 @@ namespace Avalonia.Media
if (key == null)
{
- if(SystemFonts is IFontCollection2 fontCollection2)
+ if (SystemFonts is IFontCollection2 fontCollection2)
{
if (fontCollection2.TryGetFamilyTypefaces(fontFamily.Name, out var familyTypefaces))
{
@@ -352,15 +361,23 @@ namespace Avalonia.Media
source = SystemFontsKey;
}
- if (!_fontCollections.TryGetValue(source, out fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
+ if (!_fontCollections.TryGetValue(source, out fontCollection))
{
- var embeddedFonts = new EmbeddedFontCollection(source, source);
-
- embeddedFonts.Initialize(PlatformImpl);
-
- if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
+ if (source == SystemFontsKey)
+ {
+ fontCollection = new SystemFontCollection(PlatformImpl);
+ }
+ else
+ {
+ if (source.IsAbsoluteResm() || source.IsAvares())
+ {
+ fontCollection = new EmbeddedFontCollection(source, source);
+ }
+ }
+
+ if (fontCollection != null)
{
- fontCollection = embeddedFonts;
+ return _fontCollections.TryAdd(fontCollection.Key, fontCollection);
}
}
diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
index 9efee47a5c..02701513b9 100644
--- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
+++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
@@ -1,18 +1,10 @@
using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
- public class EmbeddedFontCollection : FontCollectionBase, IFontCollection2
+ public class EmbeddedFontCollection : FontCollectionBase
{
- private readonly List _fontFamilies = new List(1);
-
private readonly Uri _key;
-
private readonly Uri _source;
public EmbeddedFontCollection(Uri key, Uri source)
@@ -20,152 +12,10 @@ namespace Avalonia.Media.Fonts
_key = key;
_source = source;
- }
-
- public override Uri Key => _key;
-
- public override FontFamily this[int index] => _fontFamilies[index];
-
- public override int Count => _fontFamilies.Count;
-
- public override void Initialize(IFontManagerImpl fontManager)
- {
- var assetLoader = AvaloniaLocator.Current.GetRequiredService();
-
- var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
-
- foreach (var fontAsset in fontAssets)
- {
- var stream = assetLoader.Open(fontAsset);
- if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
- {
- AddGlyphTypeface(glyphTypeface);
- }
- }
+ TryAddFontSource(_source);
}
- public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
- FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
- {
- var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName);
-
- style = typeface.Style;
-
- weight = typeface.Weight;
-
- stretch = typeface.Stretch;
-
- var key = new FontCollectionKey(style, weight, stretch);
-
- if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
- {
- if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
- {
- return true;
- }
-
- if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
- {
- var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
-
- if(matchedKey != key)
- {
- //Create a synthetic glyph typeface. The successfull result will be cached.
- if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
- {
- glyphTypeface = syntheticGlyphTypeface;
- }
- else
- {
- //Add the matched glyph typeface to the cache
- glyphTypefaces.TryAdd(key, glyphTypeface);
- }
- }
-
- return true;
- }
- }
-
- //Try to find a partially matching font
- for (var i = 0; i < Count; i++)
- {
- var fontFamily = _fontFamilies[i];
-
- if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
- {
- if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
- TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
- {
- return true;
- }
- }
- }
-
- glyphTypeface = null;
-
- return false;
- }
-
- public override IEnumerator GetEnumerator() => _fontFamilies.GetEnumerator();
-
- private void AddGlyphTypeface(IGlyphTypeface glyphTypeface)
- {
- if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
- {
- //Add the TypographicFamilyName to the cache
- if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
- {
- AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface);
- }
-
- foreach (var kvp in glyphTypeface2.FamilyNames)
- {
- AddGlyphTypefaceByFamilyName(kvp.Value, glyphTypeface);
- }
- }
- else
- {
- AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
- }
-
- return;
-
- void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
- {
- var typefaces = _glyphTypefaceCache.GetOrAdd(familyName,
- x =>
- {
- _fontFamilies.Add(new FontFamily(_key, familyName));
-
- return new ConcurrentDictionary();
- });
-
- typefaces.TryAdd(
- new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch),
- glyphTypeface);
- }
- }
-
- public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces)
- {
- familyTypefaces = null;
-
- if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
- {
- var typefaces = new List(glyphTypefaces.Count);
-
- foreach (var key in glyphTypefaces.Keys)
- {
- typefaces.Add(new Typeface(new FontFamily(_key, familyName), key.Style, key.Weight, key.Stretch));
- }
-
- familyTypefaces = typefaces;
-
- return true;
- }
-
- return false;
- }
+ public override Uri Key => _key;
}
}
diff --git a/src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs
new file mode 100644
index 0000000000..cf9717c81a
--- /dev/null
+++ b/src/Avalonia.Base/Media/Fonts/EmptySystemFontCollection.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Avalonia.Media.Fonts
+{
+ internal class EmptySystemFontCollection : FontCollectionBase
+ {
+ public override Uri Key => FontManager.SystemFontsKey;
+ }
+}
diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
index 222a514ed1..8f2b43ff8d 100644
--- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
+++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
@@ -4,30 +4,41 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using System.Text;
+using System.IO;
using Avalonia.Platform;
-using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
- public abstract class FontCollectionBase : IFontCollection
+ public abstract class FontCollectionBase : IFontCollection2
{
- protected readonly ConcurrentDictionary> _glyphTypefaceCache = new();
+ private static readonly Comparer FontFamilyNameComparer =
+ Comparer.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
- public abstract Uri Key { get; }
+ // Make this internal for testing purposes
+ internal readonly ConcurrentDictionary> _glyphTypefaceCache = new();
+
+ private readonly object _fontFamiliesLock = new();
+ private volatile FontFamily[] _fontFamilies = Array.Empty();
+ private readonly IFontManagerImpl _fontManagerImpl;
+ private readonly IAssetLoader _assetLoader;
+
+ protected FontCollectionBase()
+ {
+ _fontManagerImpl = AvaloniaLocator.Current.GetRequiredService();
+ _assetLoader = AvaloniaLocator.Current.GetRequiredService();
+ }
- public abstract int Count { get; }
+ public abstract Uri Key { get; }
- public abstract FontFamily this[int index] { get; }
+ public int Count => _fontFamilies.Length;
- public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch,
- [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+ public FontFamily this[int index] => _fontFamilies[index];
public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
string? familyName, CultureInfo? culture, out Typeface match)
{
match = default;
-
+
//If a font family is defined we try to find a match inside that family first
if (familyName != null && _glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
@@ -45,7 +56,7 @@ namespace Avalonia.Media.Fonts
//Try to find a match in any font family
foreach (var pair in _glyphTypefaceCache)
{
- if(pair.Key == familyName)
+ if (pair.Key == familyName)
{
//We already tried this before
continue;
@@ -57,7 +68,11 @@ namespace Avalonia.Media.Fonts
{
if (glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
- match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), style, weight, stretch);
+ // Found a match
+ match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName),
+ glyphTypeface.Style,
+ glyphTypeface.Weight,
+ glyphTypeface.Stretch);
return true;
}
@@ -69,9 +84,9 @@ namespace Avalonia.Media.Fonts
public virtual bool TryCreateSyntheticGlyphTypeface(
IGlyphTypeface glyphTypeface,
- FontStyle style,
- FontWeight weight,
- FontStretch stretch,
+ FontStyle style,
+ FontWeight weight,
+ FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{
syntheticGlyphTypeface = null;
@@ -82,8 +97,6 @@ namespace Avalonia.Media.Fonts
return false;
}
- var fontManager = FontManager.Current.PlatformImpl;
-
var key = new FontCollectionKey(style, weight, stretch);
var currentKey =
@@ -115,17 +128,17 @@ namespace Avalonia.Media.Fonts
{
using (stream)
{
- if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
+ if (_fontManagerImpl.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
{
//Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
- AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface);
+ TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, syntheticGlyphTypeface);
}
foreach (var kvp in glyphTypeface2.FamilyNames)
{
- AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface);
+ TryAddGlyphTypeface(kvp.Value, key, syntheticGlyphTypeface);
}
return true;
@@ -136,46 +149,420 @@ namespace Avalonia.Media.Fonts
}
return false;
+ }
+
+ public IEnumerator GetEnumerator() => ((IEnumerable)_fontFamilies).GetEnumerator();
+
+ public virtual bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+ FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+ {
+ var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
+
+ style = typeface.Style;
+
+ weight = typeface.Weight;
- void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
+ stretch = typeface.Stretch;
+
+ var key = new FontCollectionKey(style, weight, stretch);
+
+ return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
+ }
+
+ public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces)
+ {
+ familyTypefaces = null;
+
+ if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
- var typefaces = _glyphTypefaceCache.GetOrAdd(familyName,
- x =>
- {
- return new ConcurrentDictionary();
- });
+ // Take a snapshot of the entries to avoid issues with concurrent modifications
+ var entries = glyphTypefaces.ToArray();
- typefaces.TryAdd(key, glyphTypeface);
+ var typefaces = new Typeface[entries.Length];
+
+ for (var i = 0; i < entries.Length; i++)
+ {
+ var key = entries[i].Key;
+
+ typefaces[i] = new Typeface(new FontFamily(Key + "#" + familyName), key.Style, key.Weight, key.Stretch);
+ }
+
+ familyTypefaces = typefaces;
+
+ return true;
}
+
+ return false;
}
- public abstract void Initialize(IFontManagerImpl fontManager);
+ public bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+ {
+ if (!_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+ {
+ glyphTypeface = null;
+
+ return false;
+ }
- public abstract IEnumerator GetEnumerator();
+ var key = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
- void IDisposable.Dispose()
+ return TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface);
+ }
+
+ ///
+ /// Attempts to add the specified to the font collection.
+ ///
+ /// This method checks the and, if applicable,
+ /// the typographic family name and other family names provided by the interface.
+ /// If any of these names can be associated with the glyph typeface, the typeface is added to the collection.
+ /// The method ensures that duplicate entries are not added.
+ /// The glyph typeface to add. Must not be and must have a non-empty .
+ /// if the glyph typeface was successfully added to the collection; otherwise, .
+ public bool TryAddGlyphTypeface(IGlyphTypeface glyphTypeface)
{
- foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
+ if (glyphTypeface == null || string.IsNullOrEmpty(glyphTypeface.FamilyName))
{
- foreach (var pair in glyphTypefaces)
+ return false;
+ }
+
+ var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+
+ if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
+ {
+ var result = false;
+
+ //Add the TypographicFamilyName to the cache
+ if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
- pair.Value?.Dispose();
+ if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface))
+ {
+ result = true;
+ }
+ }
+
+ foreach (var kvp in glyphTypeface2.FamilyNames)
+ {
+ if (TryAddGlyphTypeface(kvp.Value, key, glyphTypeface))
+ {
+ result = true;
+ }
}
+
+ return result;
}
+ else
+ {
+ return TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface);
+ }
+ }
- GC.SuppressFinalize(this);
+ ///
+ /// Attempts to add a glyph typeface from the specified font stream.
+ ///
+ /// The method first attempts to create a glyph typeface from the provided font stream.
+ /// If successful, it adds the created glyph typeface to the collection.
+ /// The font stream containing the font data. The stream must be readable and positioned at the beginning of the
+ /// font data.
+ /// When this method returns, contains the created instance if the operation
+ /// succeeds; otherwise, .
+ /// if the glyph typeface was successfully created and added; otherwise, .
+ public bool TryAddGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+ {
+ if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out glyphTypeface))
+ {
+ return false;
+ }
+
+ return TryAddGlyphTypeface(glyphTypeface);
}
- IEnumerator IEnumerable.GetEnumerator()
+ ///
+ /// Attempts to add a font source to the font collection.
+ ///
+ /// This method processes the specified font source and attempts to load all available
+ /// fonts from it. Fonts are added to the collection based on their family name and typographic family name (if
+ /// available). If the is , the method returns .
+ /// The URI of the font source to add. This can be a file path, a resource URI, or another valid font source
+ /// URI.
+ /// if at least one font from the specified source was successfully added to the font
+ /// collection; otherwise, .
+ public bool TryAddFontSource(Uri source)
{
- return GetEnumerator();
+ if (source is null)
+ {
+ return false;
+ }
+
+ var result = false;
+
+ switch (source.Scheme)
+ {
+ case "avares":
+ case "resm":
+ {
+ var fontAssets = FontFamilyLoader.LoadFontAssets(source);
+
+ foreach (var fontAsset in fontAssets)
+ {
+ var stream = _assetLoader.Open(fontAsset);
+
+ if (!_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
+ {
+ continue;
+ }
+
+ var key = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+
+ //Add TypographicFamilyName to the cache
+ if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
+ {
+ if (TryAddGlyphTypeface(glyphTypeface2.TypographicFamilyName, key, glyphTypeface))
+ {
+ result = true;
+ }
+ }
+
+ if (TryAddGlyphTypeface(glyphTypeface.FamilyName, key, glyphTypeface))
+ {
+ result = true;
+ }
+ }
+
+ break;
+ }
+ case "file":
+ {
+ // If the path is a file, load the font file directly
+ if (FontFamilyLoader.IsFontSource(source))
+ {
+ if (!File.Exists(source.LocalPath))
+ {
+ return false;
+ }
+
+ using var stream = File.OpenRead(source.LocalPath);
+
+ if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
+ {
+ if (TryAddGlyphTypeface(glyphTypeface))
+ {
+ result = true;
+ }
+ }
+ }
+ // If the path is a directory, load all font files from that directory
+ else
+ {
+ if (!Directory.Exists(source.LocalPath))
+ {
+ return false;
+ }
+
+ foreach (var file in Directory.EnumerateFiles(source.LocalPath))
+ {
+ if (FontFamilyLoader.IsFontFile(file))
+ {
+ using var stream = File.OpenRead(file);
+
+ if (_fontManagerImpl.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
+ {
+ if (TryAddGlyphTypeface(glyphTypeface))
+ {
+ result = true;
+ }
+ }
+ }
+ }
+ }
+
+ break;
+ }
+ default:
+ //Unsupported scheme
+ return false;
+ }
+
+ return result;
}
- internal static bool TryGetNearestMatch(
- ConcurrentDictionary glyphTypefaces,
- FontCollectionKey key,
- [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+ ///
+ /// Inserts the specified font family into the internal collection, maintaining the collection in sorted order
+ /// by font family name.
+ ///
+ /// If a font family with the same name already exists in the collection, the new
+ /// instance will be inserted alongside it. The collection remains sorted after insertion.
+ /// The font family to add to the collection. Cannot be null.
+ protected void AddFontFamily(FontFamily fontFamily)
+ {
+ if (fontFamily == null)
+ {
+ throw new ArgumentNullException(nameof(fontFamily));
+ }
+
+ lock (_fontFamiliesLock)
+ {
+ var current = _fontFamilies;
+ int index = Array.BinarySearch(current, fontFamily, FontFamilyNameComparer);
+
+ // If an existing family with the same name is present, do nothing
+ if (index >= 0)
+ {
+ // BinarySearch found an equal entry, so avoid
+ // allocating a new array and inserting a duplicate.
+ return;
+ }
+
+ index = ~index;
+
+ var copy = new FontFamily[current.Length + 1];
+
+ if (index > 0)
+ {
+ Array.Copy(current, 0, copy, 0, index);
+ }
+
+ copy[index] = fontFamily;
+
+ if (index < current.Length)
+ {
+ Array.Copy(current, index, copy, index + 1, current.Length - index);
+ }
+
+ // Publish new array for readers
+ _fontFamilies = copy;
+ }
+ }
+
+ ///
+ /// Attempts to retrieve a glyph typeface that matches the specified font family name and font collection key.
+ ///
+ /// This method performs a binary search to locate font families with names that match
+ /// the specified . If multiple matches are found, the method iterates over them to
+ /// find the best match based on the provided .
+ /// The name of the font family to search for. This parameter is case-insensitive.
+ /// The key representing the desired font collection attributes.
+ /// When this method returns, contains the matching if a match is found; otherwise,
+ /// .
+ /// if a matching glyph typeface is found; otherwise, .
+ protected bool TryGetGlyphTypeface(string familyName, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+ {
+ glyphTypeface = null;
+
+ if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+ {
+ if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
+ {
+ return true;
+ }
+
+ if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+ {
+ var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
+
+ if (matchedKey != key)
+ {
+ if (TryCreateSyntheticGlyphTypeface(glyphTypeface, key.Style, key.Weight, key.Stretch, out var syntheticGlyphTypeface))
+ {
+ glyphTypeface = syntheticGlyphTypeface;
+ }
+ else
+ {
+ // Cache the nearest match for future lookups
+ TryAddGlyphTypeface(familyName, key, glyphTypeface);
+ }
+ }
+
+ return true;
+ }
+ }
+
+ // Binary search for the first possible prefix match using the snapshot array
+ var snapshot = _fontFamilies;
+ int left = 0;
+ int right = snapshot.Length - 1;
+ int firstMatch = -1;
+
+ while (left <= right)
+ {
+ int mid = (left + right) / 2;
+
+ var compare = string.Compare(snapshot[mid].Name, familyName, StringComparison.OrdinalIgnoreCase);
+
+ // If the current name is lexicographically less than the search name, move right
+ if (compare < 0)
+ {
+ left = mid + 1;
+ }
+ else if (compare == 0)
+ {
+ // Exact match found in snapshot. Use the exact family name for lookup
+ if (_glyphTypefaceCache.TryGetValue(snapshot[mid].Name, out var exactGlyphTypefaces) &&
+ TryGetNearestMatch(exactGlyphTypefaces, key, out glyphTypeface))
+ {
+ return true;
+ }
+
+ // Exact family present but no matching typeface found.
+ return false;
+ }
+ else
+ {
+ // Only check for prefix when snapshot[mid].Name is > familyName. This
+ // avoids the more expensive StartsWith call for names that are definitely
+ // ordered before the search term.
+ if (snapshot[mid].Name.StartsWith(familyName, StringComparison.OrdinalIgnoreCase))
+ {
+ firstMatch = mid;
+ right = mid - 1; // Continue searching to the left for the first match
+ }
+ else
+ {
+ right = mid - 1;
+ }
+ }
+ }
+
+ if (firstMatch != -1)
+ {
+ // Iterate over all consecutive prefix matches
+ for (int i = firstMatch; i < snapshot.Length; i++)
+ {
+ var fontFamily = snapshot[i];
+
+ if (!fontFamily.Name.StartsWith(familyName, StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
+ TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Attempts to retrieve the nearest matching for the specified font key from the
+ /// provided collection of glyph typefaces.
+ ///
+ /// This method attempts to find the best match for the specified font key by considering
+ /// various fallback strategies, such as normalizing the font style, stretch, and weight. If no suitable match is found, the method will return the first available non-null from the
+ /// collection, if any.
+ /// A collection of glyph typefaces, indexed by .
+ /// The representing the desired font attributes.
+ /// When this method returns, contains the that most closely matches the specified
+ /// key, if a match is found; otherwise, .
+ /// if a matching is found; otherwise, .
+ protected bool TryGetNearestMatch(IDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null)
{
@@ -218,7 +605,7 @@ namespace Avalonia.Media.Fonts
//Take the first glyph typeface we can find.
foreach (var typeface in glyphTypefaces.Values)
{
- if(typeface != null)
+ if (typeface != null)
{
glyphTypeface = typeface;
@@ -229,11 +616,86 @@ namespace Avalonia.Media.Fonts
return false;
}
- internal static bool TryFindStretchFallback(
- ConcurrentDictionary glyphTypefaces,
- FontCollectionKey key,
- [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+ ///
+ /// Attempts to add a glyph typeface to the cache for the specified font family and key.
+ ///
+ /// If the specified font family does not exist in the cache, it is added along with the
+ /// glyph typeface. The method ensures that the font family is inserted in a sorted order within the internal
+ /// collection.
+ /// The name of the font family to which the glyph typeface belongs. Cannot be null or empty.
+ /// The key associated with the glyph typeface in the cache.
+ /// The glyph typeface to add to the cache. Can be null.
+ /// if the glyph typeface was successfully added to the cache; otherwise, .
+ protected bool TryAddGlyphTypeface(string familyName, FontCollectionKey key, IGlyphTypeface? glyphTypeface)
+ {
+ if (string.IsNullOrEmpty(familyName))
+ {
+ return false;
+ }
+
+ // Check if the family already exists
+ if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+ {
+ if (glyphTypefaces.TryGetValue(key, out var existing))
+ {
+ if (ReferenceEquals(existing, glyphTypeface) || (existing is null && glyphTypeface is null))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ return glyphTypefaces.TryAdd(key, glyphTypeface);
+ }
+
+ // Family doesn't exist yet. Create a new dictionary instance and try to install it.
+ var newDict = new ConcurrentDictionary();
+
+ // GetOrAdd will return the instance that ended up in the dictionary. If it's our
+ // newDict instance then we won the race to add the family and should publish it.
+ var dict = _glyphTypefaceCache.GetOrAdd(familyName, newDict);
+
+ if (ReferenceEquals(dict, newDict))
+ {
+ // We successfully installed the dictionary; publish the FontFamily once.
+ var fontFamily = new FontFamily(Key + "#" + familyName);
+
+ // Add the font family to the sorted array
+ AddFontFamily(fontFamily);
+ }
+
+ // Add or compare the glyphTypeface in the resulting dictionary.
+ if (dict.TryGetValue(key, out var existingAfter))
+ {
+ if (ReferenceEquals(existingAfter, glyphTypeface) || (existingAfter is null && glyphTypeface is null))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ return dict.TryAdd(key, glyphTypeface);
+ }
+
+ ///
+ /// Attempts to locate a fallback glyph typeface with a similar font stretch to the specified key within the
+ /// provided collection.
+ ///
+ /// The search prioritizes font stretches closest to the requested value, expanding
+ /// outward until a match is found or all options are exhausted.
+ /// A dictionary mapping font collection keys to their corresponding glyph typefaces. Used as the source for
+ /// searching fallback typefaces.
+ /// The font collection key specifying the desired font stretch and other font attributes to match.
+ /// When this method returns, contains the found glyph typeface with a similar stretch if one exists; otherwise,
+ /// null.
+ /// true if a suitable fallback glyph typeface is found; otherwise, false.
+ private static bool TryFindStretchFallback(
+ IDictionary glyphTypefaces,
+ FontCollectionKey key,
+ [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
@@ -263,9 +725,22 @@ namespace Avalonia.Media.Fonts
return false;
}
- internal static bool TryFindWeightFallback(
- ConcurrentDictionary glyphTypefaces,
+ ///
+ /// Attempts to locate a fallback glyph typeface in the specified collection that closely matches the weight of
+ /// the provided key.
+ ///
+ /// The method searches for the closest available weight to the requested value,
+ /// considering both lighter and heavier alternatives within the collection. If no exact match is found, it
+ /// progressively searches for the nearest available weight in both directions.
+ /// A dictionary mapping font collection keys to glyph typeface instances. The method searches this collection
+ /// for a suitable fallback.
+ /// The font collection key specifying the desired font attributes, including weight, for which a fallback glyph
+ /// typeface is sought.
+ /// When this method returns, contains the matching glyph typeface if a suitable fallback is found; otherwise,
+ /// null.
+ /// true if a fallback glyph typeface matching the requested weight is found; otherwise, false.
+ private static bool TryFindWeightFallback(
+ IDictionary glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
@@ -348,68 +823,22 @@ namespace Avalonia.Media.Fonts
return false;
}
- internal static Typeface GetImplicitTypeface(Typeface typeface, out string normalizedFamilyName)
+ void IDisposable.Dispose()
{
- normalizedFamilyName = typeface.FontFamily.FamilyNames.PrimaryFamilyName;
-
- //Return early if no separator is present.
- if (!normalizedFamilyName.Contains(' '))
- {
- return typeface;
- }
-
- var style = typeface.Style;
- var weight = typeface.Weight;
- var stretch = typeface.Stretch;
-
- StringBuilder? normalizedFamilyNameBuilder = null;
- var totalCharsRemoved = 0;
-
- var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' ');
-
- // Skip initial family name.
- tokenizer.ReadSpan();
-
- while (tokenizer.TryReadSpan(out var token))
+ foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
{
- // Don't try to match numbers.
- if (new SpanStringTokenizer(token).TryReadInt32(out _))
- {
- continue;
- }
-
- // Try match with font style, weight or stretch and update accordingly.
- var match = false;
- if (EnumHelper.TryParse(token, true, out var newStyle))
- {
- style = newStyle;
- match = true;
- }
- else if (EnumHelper.TryParse(token, true, out var newWeight))
- {
- weight = newWeight;
- match = true;
- }
- else if (EnumHelper.TryParse(token, true, out var newStretch))
- {
- stretch = newStretch;
- match = true;
- }
-
- if (match)
+ foreach (var pair in glyphTypefaces)
{
- // Carve out matched word from the normalized name.
- normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName);
- normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length);
- totalCharsRemoved += token.Length;
+ pair.Value?.Dispose();
}
}
- // Get rid of any trailing spaces.
- normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd();
+ GC.SuppressFinalize(this);
+ }
- //Preserve old font source
- return new Typeface(typeface.FontFamily, style, weight, stretch);
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
}
}
}
diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
index 2c61ffd419..99b5defa5a 100644
--- a/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
+++ b/src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
@@ -6,7 +6,7 @@ using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
- public static class FontFamilyLoader
+ internal static class FontFamilyLoader
{
///
/// Loads all font assets that belong to the specified
@@ -17,7 +17,7 @@ namespace Avalonia.Media.Fonts
{
if (source.IsAvares() || source.IsAbsoluteResm())
{
- return IsFontTtfOrOtf(source) ?
+ return IsFontSource(source) ?
GetFontAssetsByExpression(source) :
GetFontAssetsBySource(source);
}
@@ -25,6 +25,35 @@ namespace Avalonia.Media.Fonts
return Enumerable.Empty();
}
+ ///
+ /// Determines whether the specified URI refers to a font file source.
+ ///
+ /// The URI to evaluate as a potential font file source. Must not be null.
+ /// true if the URI points to a recognized font file source; otherwise, false.
+ public static bool IsFontSource(Uri uri)
+ {
+ var sourceWithoutArguments = GetSubString(uri.OriginalString, '?');
+ return IsFontFile(sourceWithoutArguments);
+ }
+
+ ///
+ /// Determines whether the specified file path refers to a supported font file type.
+ ///
+ /// This method performs a case-insensitive check for common font file extensions. It
+ /// does not verify the existence or validity of the file at the specified path.
+ /// The path of the file to check. Can be a relative or absolute path. If null, the method returns false.
+ /// true if the file path ends with ".ttf", ".otf", or ".ttc" (case-insensitive); otherwise, false.
+ public static bool IsFontFile(string filePath)
+ {
+ if (filePath is null)
+ {
+ return false;
+ }
+
+ return filePath.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase)
+ || filePath.EndsWith(".otf", StringComparison.OrdinalIgnoreCase)
+ || filePath.EndsWith(".ttc", StringComparison.OrdinalIgnoreCase);
+ }
///
/// Searches for font assets at a given location and returns a quantity of found assets
@@ -35,7 +64,7 @@ namespace Avalonia.Media.Fonts
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService();
var availableAssets = assetLoader.GetAssets(source, null);
- return availableAssets.Where(x => IsFontTtfOrOtf(x));
+ return availableAssets.Where(x => IsFontSource(x));
}
///
@@ -97,13 +126,6 @@ namespace Avalonia.Media.Fonts
&& path.EndsWith(fileExtension, StringComparison.OrdinalIgnoreCase);
}
- private static bool IsFontTtfOrOtf(Uri uri)
- {
- var sourceWithoutArguments = GetSubString(uri.OriginalString, '?');
- return sourceWithoutArguments.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase)
- || sourceWithoutArguments.EndsWith(".otf", StringComparison.OrdinalIgnoreCase);
- }
-
private static (string fileNameWithoutExtension, string extension) GetFileNameAndExtension(
string path, char directorySeparator = '/')
{
diff --git a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs
index 2a30f0abd8..4579cb5a34 100644
--- a/src/Avalonia.Base/Media/Fonts/IFontCollection.cs
+++ b/src/Avalonia.Base/Media/Fonts/IFontCollection.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
@@ -13,12 +12,6 @@ namespace Avalonia.Media.Fonts
///
Uri Key { get; }
- ///
- /// Initializes the font collection.
- ///
- /// The font manager the collection is registered with.
- void Initialize(IFontManagerImpl fontManager);
-
///
/// Try to get a glyph typeface for given parameters.
///
@@ -70,5 +63,23 @@ namespace Avalonia.Media.Fonts
///
/// Returns true if a synthetic glyph typface can be created; otherwise, false
bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface);
+
+ ///
+ /// Attempts to retrieve the glyph typeface that most closely matches the specified font family name, style,
+ /// weight, and stretch.
+ ///
+ /// This method searches for a glyph typeface in the font collection cache that matches
+ /// the specified parameters. If an exact match is not found, fallback mechanisms are applied to find the
+ /// closest available match based on the specified style, weight, and stretch. If no suitable match is found,
+ /// the method returns and is set to .
+ /// The name of the font family to search for. This parameter cannot be or empty.
+ /// The desired font style.
+ /// The desired font weight.
+ /// The desired font stretch.
+ /// When this method returns, contains the that most closely matches the specified
+ /// parameters, if a match is found; otherwise, . This parameter is passed uninitialized.
+ /// if a matching glyph typeface is found; otherwise, .
+ bool TryGetNearestMatch(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
}
}
diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
index 3b0c71ce20..7ff8df9951 100644
--- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
+++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -8,45 +7,33 @@ using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
- internal class SystemFontCollection : FontCollectionBase, IFontCollection2
+ internal class SystemFontCollection : FontCollectionBase
{
- private readonly FontManager _fontManager;
- private readonly List _familyNames;
+ private readonly IFontManagerImpl _platformImpl;
- public SystemFontCollection(FontManager fontManager)
+ public SystemFontCollection(IFontManagerImpl platformImpl)
{
- _fontManager = fontManager;
- _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x)).ToList();
- }
+ _platformImpl = platformImpl ?? throw new ArgumentNullException(nameof(platformImpl));
- public override Uri Key => FontManager.SystemFontsKey;
+ var familyNames = _platformImpl.GetInstalledFontFamilyNames().Where(x => !string.IsNullOrEmpty(x));
- public override FontFamily this[int index]
- {
- get
+ foreach (var familyName in familyNames)
{
- var familyName = _familyNames[index];
-
- return new FontFamily(familyName);
+ AddFontFamily(familyName);
}
}
- public override int Count => _familyNames.Count;
-
- public override IEnumerator GetEnumerator()
- {
- foreach (var familyName in _familyNames)
- {
- yield return new FontFamily(familyName);
- }
- }
+ public override Uri Key => FontManager.SystemFontsKey;
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
- glyphTypeface = null;
+ var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
- var typeface = GetImplicitTypeface(new Typeface(familyName, style, weight, stretch), out familyName);
+ if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
+ {
+ return true;
+ }
style = typeface.Style;
@@ -56,123 +43,68 @@ namespace Avalonia.Media.Fonts
var key = new FontCollectionKey(style, weight, stretch);
- if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+ //Check cache first to avoid unnecessary calls to the font manager
+ if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
- if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
- {
- return glyphTypeface != null;
- }
+ return glyphTypeface != null;
}
- glyphTypefaces ??= _glyphTypefaceCache.GetOrAdd(familyName,
- (_) => new ConcurrentDictionary());
-
//Try to create the glyph typeface via system font manager
- if (!_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch,
- out glyphTypeface))
+ if (!_platformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
- glyphTypefaces.TryAdd(key, null);
+ //Add null to cache to avoid future calls
+ TryAddGlyphTypeface(familyName, key, null);
return false;
}
- var createdKey =
- new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
-
- //No exact match
- if (createdKey != key)
+ //Add to cache
+ if (!TryAddGlyphTypeface(glyphTypeface))
{
- //Add the created glyph typeface to the cache so we can match it.
- glyphTypefaces.TryAdd(createdKey, glyphTypeface);
-
- //Try to find nearest match if possible
- if (TryGetNearestMatch(glyphTypefaces, key, out var nearestMatch))
- {
- glyphTypeface = nearestMatch;
- }
-
- //Try to create a synthetic glyph typeface
- if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
- {
- glyphTypeface = syntheticGlyphTypeface;
-
- return true;
- }
+ return false;
}
- glyphTypefaces.TryAdd(key, glyphTypeface);
-
- return glyphTypeface != null;
+ //Requested glyph typeface should be in cache now
+ return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
}
- public override void Initialize(IFontManagerImpl fontManager)
+ public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces)
{
- //We initialize the system font collection during construction.
- }
+ familyTypefaces = null;
- public void AddCustomFontSource(Uri source)
- {
- if (source is null)
+ if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
{
- return;
+ return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces);
}
- LoadGlyphTypefaces(_fontManager.PlatformImpl, source);
+ return false;
}
- private void LoadGlyphTypefaces(IFontManagerImpl fontManager, Uri source)
+ public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName,
+ CultureInfo? culture, out Typeface match)
{
- var assetLoader = AvaloniaLocator.Current.GetRequiredService();
-
- var fontAssets = FontFamilyLoader.LoadFontAssets(source);
+ var requestedKey = new FontCollectionKey { Style = style, Weight = weight, Stretch = stretch };
- foreach (var fontAsset in fontAssets)
+ //TODO12: Think about removing familyName parameter
+ if (base.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match))
{
- var stream = assetLoader.Open(fontAsset);
+ var matchKey = new FontCollectionKey { Style = match.Style, Weight = match.Weight, Stretch = match.Stretch };
- if (!fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
+ if (requestedKey == matchKey)
{
- continue;
- }
-
- //Add TypographicFamilyName to the cache
- if (glyphTypeface is IGlyphTypeface2 glyphTypeface2 && !string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
- {
- AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, glyphTypeface);
+ return true;
}
-
- AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
}
- return;
- }
-
- public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces)
- {
- familyTypefaces = null;
-
- if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
+ if (_platformImpl is IFontManagerImpl2 fontManagerImpl2)
{
- return fontManagerImpl2.TryGetFamilyTypefaces(familyName, out familyTypefaces);
- }
-
- return false;
- }
-
- public override bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch, string? familyName,
- CultureInfo? culture, out Typeface match)
- {
- //TODO12: Think about removing familyName parameter
- match = default;
-
- if (_fontManager.PlatformImpl is IFontManagerImpl2 fontManagerImpl2)
- {
- if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, culture, out var glyphTypeface))
+ if (fontManagerImpl2.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out var glyphTypeface))
{
- AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
-
match = new Typeface(glyphTypeface.FamilyName, glyphTypeface.Style, glyphTypeface.Weight,
- glyphTypeface.Stretch);
+ glyphTypeface.Stretch);
+
+ // Add to cache if not already present
+ TryAddGlyphTypeface(glyphTypeface);
return true;
}
@@ -181,26 +113,8 @@ namespace Avalonia.Media.Fonts
}
else
{
- return _fontManager.PlatformImpl.TryMatchCharacter(codepoint, style, weight, stretch, culture, out match);
- }
- }
-
- private void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
- {
- // Add family name to the collection if not exists
- if (!_familyNames.Contains(familyName))
- {
- _familyNames.Add(familyName);
+ return _platformImpl.TryMatchCharacter(codepoint, style, weight, stretch, familyName, culture, out match);
}
-
- // Get or create the typefaces dictionary for the family name
- if (!_glyphTypefaceCache.TryGetValue(familyName, out var typefaces))
- {
- _glyphTypefaceCache[familyName] = typefaces = new ConcurrentDictionary();
- }
-
- // Add the glyph typeface to the cache
- typefaces.TryAdd(new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), glyphTypeface);
}
}
}
diff --git a/src/Avalonia.Base/Media/Typeface.cs b/src/Avalonia.Base/Media/Typeface.cs
index 1ceff7101f..c9ad271568 100644
--- a/src/Avalonia.Base/Media/Typeface.cs
+++ b/src/Avalonia.Base/Media/Typeface.cs
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
+using System.Text;
+using Avalonia.Utilities;
namespace Avalonia.Media
{
@@ -127,5 +129,81 @@ namespace Avalonia.Media
return hashCode;
}
}
+
+ ///
+ /// Normalizes the typeface by extracting and removing style, weight, and stretch information from the font
+ /// family name, and returns a new instance with the updated properties.
+ ///
+ /// This method analyzes the font family name to identify and extract any style, weight,
+ /// or stretch information embedded within it. If such information is found, it is removed from the family name,
+ /// and the corresponding properties of the returned are updated accordingly. If no such
+ /// information is found, the method returns the current instance without modification.
+ /// When this method returns, contains the normalized font family name with style, weight, and stretch
+ /// information removed. This parameter is passed uninitialized.
+ /// A new instance with the updated , ,
+ /// and properties, or the current instance if no normalization was performed.
+ public Typeface Normalize(out string normalizedFamilyName)
+ {
+ normalizedFamilyName = FontFamily.FamilyNames.PrimaryFamilyName;
+
+ //Return early if no separator is present.
+ if (!normalizedFamilyName.Contains(' '))
+ {
+ return this;
+ }
+
+ var style = Style;
+ var weight = Weight;
+ var stretch = Stretch;
+
+ StringBuilder? normalizedFamilyNameBuilder = null;
+ var totalCharsRemoved = 0;
+
+ var tokenizer = new SpanStringTokenizer(normalizedFamilyName, ' ');
+
+ // Skip initial family name.
+ tokenizer.ReadSpan();
+
+ while (tokenizer.TryReadSpan(out var token))
+ {
+ // Don't try to match numbers.
+ if (new SpanStringTokenizer(token).TryReadInt32(out _))
+ {
+ continue;
+ }
+
+ // Try match with font style, weight or stretch and update accordingly.
+ var match = false;
+ if (EnumHelper.TryParse(token, true, out var newStyle))
+ {
+ style = newStyle;
+ match = true;
+ }
+ else if (EnumHelper.TryParse(token, true, out var newWeight))
+ {
+ weight = newWeight;
+ match = true;
+ }
+ else if (EnumHelper.TryParse(token, true, out var newStretch))
+ {
+ stretch = newStretch;
+ match = true;
+ }
+
+ if (match)
+ {
+ // Carve out matched word from the normalized name.
+ normalizedFamilyNameBuilder ??= new StringBuilder(normalizedFamilyName);
+ normalizedFamilyNameBuilder.Remove(tokenizer.CurrentTokenIndex - totalCharsRemoved, token.Length);
+ totalCharsRemoved += token.Length;
+ }
+ }
+
+ // Get rid of any trailing spaces.
+ normalizedFamilyName = (normalizedFamilyNameBuilder?.ToString() ?? normalizedFamilyName).TrimEnd();
+
+ //Preserve old font source
+ return new Typeface(FontFamily, style, weight, stretch);
+ }
}
}
diff --git a/src/Avalonia.Base/Platform/IFontManagerImpl.cs b/src/Avalonia.Base/Platform/IFontManagerImpl.cs
index 42c9b3623f..e978ba03a8 100644
--- a/src/Avalonia.Base/Platform/IFontManagerImpl.cs
+++ b/src/Avalonia.Base/Platform/IFontManagerImpl.cs
@@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
-using Avalonia.Media.Fonts;
using Avalonia.Metadata;
namespace Avalonia.Platform
@@ -29,13 +28,14 @@ namespace Avalonia.Platform
/// The font style.
/// The font weight.
/// The font stretch.
+ /// The family name. This is optional and can be used as an initial hint for matching.
/// The culture.
/// The matching typeface.
///
/// True, if the could match the character to specified parameters, False otherwise.
///
bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
- FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface);
+ FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface typeface);
///
/// Tries to get a glyph typeface for specified parameters.
@@ -72,13 +72,14 @@ namespace Avalonia.Platform
/// The font style.
/// The font weight.
/// The font stretch.
+ /// The family name. This is optional and can be used as an initial hint for matching.
/// The culture.
/// The matching typeface.
///
/// True, if the could match the character to specified parameters, False otherwise.
///
bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
- FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface);
+ FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, [NotNullWhen(true)] out IGlyphTypeface? typeface);
///
/// Tries to get a list of typefaces for the specified family name.
diff --git a/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs b/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs
index 2e9cb2bcbf..2e626e7189 100644
--- a/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs
+++ b/src/Avalonia.Controls/SystemFontAppBuilderExtension.cs
@@ -11,7 +11,7 @@ namespace Avalonia
{
if(fontManager.SystemFonts is SystemFontCollection systemFontCollection)
{
- systemFontCollection.AddCustomFontSource(fontSource);
+ systemFontCollection.TryAddFontSource(fontSource);
}
});
}
diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
index eaf0dac909..167d3fb8c7 100644
--- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -216,8 +216,7 @@ namespace Avalonia.Headless
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
- FontStretch fontStretch,
- CultureInfo? culture, out Typeface fontKey)
+ FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
{
fontKey = new Typeface(_defaultFamilyName);
@@ -281,8 +280,7 @@ namespace Avalonia.Headless
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
- FontStretch fontStretch,
- CultureInfo? culture, out Typeface fontKey)
+ FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
{
fontKey = new Typeface(_defaultFamilyName);
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
index e013124cf1..f8cc9f5f78 100644
--- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
-using System.Text.RegularExpressions;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
@@ -35,9 +34,9 @@ namespace Avalonia.Skia
[ThreadStatic] private static string[]? t_languageTagBuffer;
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
- FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface fontKey)
+ FontWeight fontWeight, FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
{
- if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface))
+ if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface))
{
fontKey = default;
@@ -61,10 +60,11 @@ namespace Avalonia.Skia
FontStyle fontStyle,
FontWeight fontWeight,
FontStretch fontStretch,
+ string? familyName,
CultureInfo? culture,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
- if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out SKTypeface? skTypeface))
+ if (!TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out SKTypeface? skTypeface))
{
glyphTypeface = null;
@@ -81,6 +81,7 @@ namespace Avalonia.Skia
FontStyle fontStyle,
FontWeight fontWeight,
FontStretch fontStretch,
+ string? familyName,
CultureInfo? culture,
[NotNullWhen(true)] out SKTypeface? skTypeface)
{
@@ -110,7 +111,7 @@ namespace Avalonia.Skia
t_languageTagBuffer ??= new string[1];
t_languageTagBuffer[0] = culture.Name;
- skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
+ skTypeface = _skFontManager.MatchCharacter(string.IsNullOrEmpty(familyName) ? null : familyName, skFontStyle, t_languageTagBuffer, codepoint);
return skTypeface != null;
}
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index 703496a834..3dd4bbd7c1 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -120,30 +120,22 @@ namespace Avalonia.Skia
foreach (var nameRecord in _nameTable)
{
- if(nameRecord.NameID == KnownNameIds.FontFamilyName)
+ var languageId = nameRecord.LanguageID == 0 ?
+ (ushort)CultureInfo.InvariantCulture.LCID :
+ nameRecord.LanguageID;
+
+ switch (nameRecord.NameID)
{
- if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
- {
- continue;
- }
-
- if (!familyNames.ContainsKey(nameRecord.LanguageID))
- {
- familyNames[nameRecord.LanguageID] = nameRecord.Value;
- }
- }
-
- if(nameRecord.NameID == KnownNameIds.FontSubfamilyName)
- {
- if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
- {
- continue;
- }
-
- if (!faceNames.ContainsKey(nameRecord.LanguageID))
- {
- faceNames[nameRecord.LanguageID] = nameRecord.Value;
- }
+ case KnownNameIds.FontFamilyName:
+ {
+ familyNames.TryAdd(languageId, nameRecord.Value);
+ break;
+ }
+ case KnownNameIds.FontSubfamilyName:
+ {
+ faceNames.TryAdd(languageId, nameRecord.Value);
+ break;
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs
index 826c1f292b..1b9226452d 100644
--- a/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/TypefaceTests.cs
@@ -23,5 +23,24 @@ namespace Avalonia.Base.UnitTests.Media
{
Assert.Equal(new Typeface("Font A").GetHashCode(), new Typeface("Font A").GetHashCode());
}
+
+ [InlineData("Hello World 6", "Hello World 6", FontStyle.Normal, FontWeight.Normal)]
+ [InlineData("Hello World Italic", "Hello World", FontStyle.Italic, FontWeight.Normal)]
+ [InlineData("Hello World Italic Bold", "Hello World", FontStyle.Italic, FontWeight.Bold)]
+ [InlineData("FontAwesome 6 Free Regular", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Normal)]
+ [InlineData("FontAwesome 6 Free Solid", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Solid)]
+ [InlineData("FontAwesome 6 Brands", "FontAwesome 6 Brands", FontStyle.Normal, FontWeight.Normal)]
+ [Theory]
+ public void Should_Get_Implicit_Typeface(string input, string familyName, FontStyle style, FontWeight weight)
+ {
+ var typeface = new Typeface(input);
+
+ var normalizedTypeface = typeface.Normalize(out var normalizedFamilyName);
+
+ Assert.Equal(familyName, normalizedFamilyName);
+ Assert.Equal(style, normalizedTypeface.Style);
+ Assert.Equal(weight, normalizedTypeface.Weight);
+ Assert.Equal(FontStretch.Normal, normalizedTypeface.Stretch);
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index 2ebac5b3a1..4e9af4978e 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -15,6 +15,7 @@ using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
+using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@@ -1238,7 +1239,8 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(),
- textShaperImpl: new HeadlessTextShaperStub()));
+ textShaperImpl: new HeadlessTextShaperStub(),
+ assetLoader: new StandardAssetLoader()));
}
private class ItemsControlWithContainer : ItemsControl
diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
index ce85984f1c..4e1742d2a4 100644
--- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
@@ -980,7 +980,7 @@ namespace Avalonia.Controls.UnitTests
private static IDisposable Start(TestServices services = null)
{
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
- return UnitTestApplication.Start(services ?? Services);
+ return UnitTestApplication.Start((services ?? Services).With(assetLoader: new StandardAssetLoader()));
}
private class Class1 : NotifyingBase
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
index c208c70b1d..4082bb47a1 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
@@ -13,6 +13,7 @@ using Avalonia.Data;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Layout;
+using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@@ -1351,7 +1352,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(),
- textShaperImpl: new HeadlessTextShaperStub()));
+ textShaperImpl: new HeadlessTextShaperStub(),
+ assetLoader: new StandardAssetLoader()));
}
private class TestSelector : SelectingItemsControl
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index c20943807e..42e1033b6d 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -749,7 +749,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void TextBox_CaretIndex_Persists_When_Focus_Lost()
{
- using (UnitTestApplication.Start(FocusServices))
+ using (UnitTestApplication.Start(FocusServices.With(assetLoader: new StandardAssetLoader())))
{
var target1 = new TextBox
{
@@ -2160,7 +2160,8 @@ namespace Avalonia.Controls.UnitTests
standardCursorFactory: Mock.Of(),
renderInterface: new HeadlessPlatformRenderInterface(),
textShaperImpl: new HeadlessTextShaperStub(),
- fontManagerImpl: new HeadlessFontManagerStub());
+ fontManagerImpl: new HeadlessFontManagerStub(),
+ assetLoader: new StandardAssetLoader());
internal static IControlTemplate CreateTemplate()
{
diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
index 62430deb09..ad22824333 100644
--- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
@@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Headless;
using Avalonia.Layout;
+using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
@@ -325,7 +326,8 @@ namespace Avalonia.Controls.UnitTests
TestServices.MockThreadingInterface.With(
fontManagerImpl: new HeadlessFontManagerStub(),
renderInterface: new HeadlessPlatformRenderInterface(),
- textShaperImpl: new HeadlessTextShaperStub()));
+ textShaperImpl: new HeadlessTextShaperStub(),
+ assetLoader: new StandardAssetLoader()));
}
private static (TransitioningContentControl, TestTransition) CreateTarget(object content)
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index 0033f836f1..ffb7ab9048 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
@@ -15,6 +15,7 @@ using Avalonia.Input.Platform;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates;
+using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@@ -1841,7 +1842,8 @@ namespace Avalonia.Controls.UnitTests
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new HeadlessPlatformRenderInterface(),
- textShaperImpl: new HeadlessTextShaperStub()));
+ textShaperImpl: new HeadlessTextShaperStub(),
+ assetLoader: new StandardAssetLoader()));
}
private class Node : NotifyingBase
diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
index 98a93596fa..24ab26ac74 100644
--- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
+++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
@@ -20,4 +20,9 @@
+
+
+ PreserveNewest
+
+
diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
new file mode 100644
index 0000000000..66afed7a3a
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
@@ -0,0 +1,188 @@
+using System;
+using System.IO;
+using System.Linq;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests.Media
+{
+ public class CustomFontCollectionTests
+ {
+ private const string NotoMono =
+ "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests";
+
+ [Fact]
+ public void Should_AddGlyphTypeface_By_Stream()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+ {
+ var fontManager = FontManager.Current;
+
+ var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute));
+
+ fontManager.AddFontCollection(fontCollection);
+
+ var assetLoader = AvaloniaLocator.Current.GetRequiredService();
+
+ var assets = assetLoader.GetAssets(new Uri(NotoMono, UriKind.Absolute), null).ToArray();
+
+ Assert.NotEmpty(assets);
+
+ var notoMonoLocation = assets.First();
+
+ using var notoMonoStream = assetLoader.Open(notoMonoLocation);
+
+ Assert.NotNull(notoMonoStream);
+
+ Assert.True(fontCollection.TryAddGlyphTypeface(notoMonoStream, out var glyphTypeface));
+
+ Assert.Equal("Inter", glyphTypeface.FamilyName);
+
+ Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Inter"), out var secondGlyphTypeface));
+
+ Assert.Equal(glyphTypeface, secondGlyphTypeface);
+ }
+ }
+
+ [Fact]
+ public void Should_Enumerate_FontFamilies()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+ {
+ var fontManager = FontManager.Current;
+
+ var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute));
+
+ fontManager.AddFontCollection(fontCollection);
+
+ var assetLoader = AvaloniaLocator.Current.GetRequiredService();
+
+ var assets = assetLoader.GetAssets(new Uri(NotoMono, UriKind.Absolute), null).Where(x => x.AbsolutePath.EndsWith(".ttf")).ToArray();
+
+ foreach (var asset in assets)
+ {
+ fontCollection.TryAddGlyphTypeface(assetLoader.Open(asset), out _);
+ }
+
+ var families = fontCollection.ToArray();
+
+ Assert.True(families.Length >= assets.Length);
+
+ var other = new CustomFontCollection(new Uri("fonts:other", UriKind.Absolute));
+
+ foreach (var family in families)
+ {
+ var familyTypefaces = family.FamilyTypefaces;
+
+ foreach (var typeface in familyTypefaces)
+ {
+ other.TryAddGlyphTypeface(typeface.GlyphTypeface);
+ }
+ }
+
+ Assert.Equal(families.Length, other.Count);
+
+ for (int i = 0; i < families.Length; i++)
+ {
+ Assert.Equal(families[i].Name, other[i].Name);
+ }
+ }
+ }
+
+ [Fact]
+ public void Should_AddFontSource_From_File()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+ {
+ var fontManager = FontManager.Current;
+ var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute));
+ fontManager.AddFontCollection(fontCollection);
+
+ // Path to the test font
+ var fontPath = Path.Combine(AppContext.BaseDirectory, "Assets", "Inter-Regular.ttf");
+ Assert.True(File.Exists(fontPath));
+
+ var fontUri = new Uri(fontPath, UriKind.Absolute);
+
+ // Add the font file
+ Assert.True(fontCollection.TryAddFontSource(fontUri));
+
+ // Check if the font was loaded
+ Assert.True(fontCollection.TryGetGlyphTypeface("Inter", FontStyle.Normal, FontWeight.Regular, FontStretch.Normal, out var glyphTypeface));
+ Assert.Equal("Inter", glyphTypeface.FamilyName);
+
+ // Check if the FontManager can find the font
+ Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Inter"), out var glyphTypeface2));
+ Assert.Equal(glyphTypeface, glyphTypeface2);
+ }
+ }
+
+ [Fact]
+ public void Should_AddFontSource_From_Folder()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+ {
+ var fontManager = FontManager.Current;
+ var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute));
+ fontManager.AddFontCollection(fontCollection);
+
+ // Path to the test fonts
+ var fontsFolder = Path.Combine(AppContext.BaseDirectory, "Assets");
+ Assert.True(Directory.Exists(fontsFolder));
+
+ var folderUri = new Uri(fontsFolder + Path.DirectorySeparatorChar, UriKind.Absolute);
+
+ // Add the fonts
+ Assert.True(fontCollection.TryAddFontSource(folderUri));
+
+ // Check if the font was loaded
+ Assert.True(fontCollection.TryGetGlyphTypeface("Inter", FontStyle.Normal, FontWeight.Regular, FontStretch.Normal, out var glyphTypeface));
+ Assert.Equal("Inter", glyphTypeface.FamilyName);
+
+ // Check if the FontManager can find the font
+ Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Inter"), out var glyphTypeface2));
+ Assert.Equal(glyphTypeface, glyphTypeface2);
+ }
+ }
+
+ [Fact]
+ public void Should_AddFontSource_From_Resource()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+ {
+ var fontManager = FontManager.Current;
+ var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute));
+ fontManager.AddFontCollection(fontCollection);
+
+ // Use the NotoMono resource as FontSource
+ var notoMonoUri = new Uri(NotoMono, UriKind.Absolute);
+
+ // Add the font resource
+ Assert.True(fontCollection.TryAddFontSource(notoMonoUri));
+
+ // Get the loaded family names
+ var families = fontCollection.ToArray();
+
+ Assert.NotEmpty(families);
+
+ // Try to get a GlyphTypeface
+ Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", FontStyle.Normal, FontWeight.Regular, FontStretch.Normal, out var glyphTypeface));
+ Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
+
+ // Check if the FontManager can find the font
+ Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Noto Mono"), out var glyphTypeface2));
+ Assert.Equal(glyphTypeface, glyphTypeface2);
+ }
+ }
+
+
+
+ private class CustomFontCollection(Uri key) : FontCollectionBase
+ {
+ public override Uri Key { get; } = key;
+ }
+ }
+}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
index 34dc32ac6b..008f1eedbd 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
@@ -13,18 +14,28 @@ namespace Avalonia.Skia.UnitTests.Media
public class CustomFontManagerImpl : IFontManagerImpl, IDisposable
{
private readonly string _defaultFamilyName;
- private readonly IFontCollection _customFonts;
- private bool _isInitialized;
+ private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
+ private IFontCollection? _systemFonts;
public CustomFontManagerImpl()
{
- var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
+ _defaultFamilyName = FontManager.SystemFontsKey + "#Noto Mono";
+ }
- _defaultFamilyName = source.AbsoluteUri + "#Noto Mono";
+ public IFontCollection SystemFonts
+ {
+ get
+ {
+ if(_systemFonts is null)
+ {
+ var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
- _customFonts = new EmbeddedFontCollection(source, source);
- }
+ _systemFonts = new EmbeddedFontCollection(FontManager.SystemFontsKey, source);
+ }
+ return _systemFonts;
+ }
+ }
public string GetDefaultFontFamilyName()
{
return _defaultFamilyName;
@@ -32,27 +43,46 @@ namespace Avalonia.Skia.UnitTests.Media
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
- if (!_isInitialized)
+ // Directly load from assets to avoid creating the full font collection
+ try
{
- _customFonts.Initialize(this);
-
- _isInitialized = true;
+ var key = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests");
+
+ var assetLoader = AvaloniaLocator.Current.GetRequiredService();
+
+ var fontAssets = FontFamilyLoader.LoadFontAssets(key);
+ var names = new HashSet(StringComparer.InvariantCultureIgnoreCase);
+
+ foreach (var fontAsset in fontAssets)
+ {
+ try
+ {
+ using var stream = assetLoader.Open(fontAsset);
+ using var sk = SKTypeface.FromStream(stream);
+
+ if (sk != null && !string.IsNullOrEmpty(sk.FamilyName))
+ {
+ names.Add(sk.FamilyName);
+ }
+ }
+ catch
+ {
+ // Ignore faulty assets
+ }
+ }
+
+ return names.ToArray();
+ }
+ catch
+ {
+ return Array.Empty();
}
-
- return _customFonts.Select(x=> x.Name).ToArray();
}
- private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
-
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
- CultureInfo culture, out Typeface typeface)
+ string? familyName, CultureInfo? culture, out Typeface typeface)
{
- if (!_isInitialized)
- {
- _customFonts.Initialize(this);
- }
-
- if(_customFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, null, culture, out typeface))
+ if(SystemFonts.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
return true;
}
@@ -68,12 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
- if (!_isInitialized)
- {
- _customFonts.Initialize(this);
- }
-
- if (_customFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
+ if (SystemFonts.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
return true;
}
@@ -97,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media
public void Dispose()
{
- _customFonts.Dispose();
+ _systemFonts?.Dispose();
}
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
index 75ad5b1a10..ef55f1d6df 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
@@ -14,13 +14,8 @@ namespace Avalonia.Skia.UnitTests.Media
{
public class EmbeddedFontCollectionTests
{
- private const string s_notoMono =
- "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
-
- private const string s_manrope = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Manrope";
-
- private const string s_misans = "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#MiSans";
-
+ private const string s_fontAssets =
+ "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests";
[InlineData(FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(FontWeight.Bold, FontStyle.Italic)]
@@ -28,14 +23,13 @@ namespace Avalonia.Skia.UnitTests.Media
[Theory]
public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle)
{
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
- var source = new Uri(s_notoMono, UriKind.Absolute);
+ var key = new Uri("fonts:testFonts", UriKind.Absolute);
+ var source = new Uri(s_fontAssets, UriKind.Absolute);
var fontCollection = new TestEmbeddedFontCollection(source, source);
- fontCollection.Initialize(new CustomFontManagerImpl());
-
Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface));
var actual = glyphTypeface.FamilyName;
@@ -47,13 +41,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Not_Get_Typeface_For_Invalid_FamilyName()
{
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
- var source = new Uri(s_notoMono, UriKind.Absolute);
-
- var fontCollection = new TestEmbeddedFontCollection(source, source);
+ var key = new Uri("fonts:testFonts", UriKind.Absolute);
+ var source = new Uri(s_fontAssets, UriKind.Absolute);
- fontCollection.Initialize(new CustomFontManagerImpl());
+ var fontCollection = new TestEmbeddedFontCollection(key, source);
Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out _));
}
@@ -62,13 +55,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Get_Typeface_For_Partial_FamilyName()
{
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
- var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute);
+ var key = new Uri("fonts:testFonts", UriKind.Absolute);
+ var source = new Uri(s_fontAssets, UriKind.Absolute);
- var fontCollection = new TestEmbeddedFontCollection(source, source);
-
- fontCollection.Initialize(new CustomFontManagerImpl());
+ var fontCollection = new TestEmbeddedFontCollection(key, source);
Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
@@ -79,13 +71,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Get_Typeface_For_TypographicFamilyName()
{
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
- var source = new Uri(s_manrope, UriKind.Absolute);
-
- var fontCollection = new TestEmbeddedFontCollection(source, source);
+ var key = new Uri("fonts:testFonts", UriKind.Absolute);
+ var source = new Uri(s_fontAssets, UriKind.Absolute);
- fontCollection.Initialize(new CustomFontManagerImpl());
+ var fontCollection = new TestEmbeddedFontCollection(key, source);
Assert.True(fontCollection.TryGetGlyphTypeface("Manrope", FontStyle.Normal, FontWeight.Light, FontStretch.Normal, out var glyphTypeface));
@@ -102,13 +93,12 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Cache_Synthetic_GlyphTypeface()
{
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl())))
{
- var source = new Uri(s_manrope, UriKind.Absolute);
-
- var fontCollection = new TestEmbeddedFontCollection(source, source, true);
+ var key = new Uri("fonts:testFonts", UriKind.Absolute);
+ var source = new Uri(s_fontAssets, UriKind.Absolute);
- fontCollection.Initialize(new CustomFontManagerImpl());
+ var fontCollection = new TestEmbeddedFontCollection(key, source, true);
Assert.True(fontCollection.TryGetGlyphTypeface("Manrope", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface));
@@ -125,18 +115,19 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Cache_Nearest_Match_For_MiSans()
{
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
- var source = new Uri(s_misans, UriKind.Absolute);
+ var source = new Uri(s_fontAssets, UriKind.Absolute);
var fontCollection = new TestEmbeddedFontCollection(source, source);
- fontCollection.Initialize(new CustomFontManagerImpl());
-
+ // Font weight 304
Assert.True(fontCollection.TryGetGlyphTypeface("MiSans", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var regularGlyphTypeface));
+ // Font weight regular (400)
Assert.True(fontCollection.TryGetGlyphTypeface("MiSans", FontStyle.Normal, FontWeight.Bold, FontStretch.Normal, out var boldGlyphTypeface));
+ // Font weight 700
Assert.True(fontCollection.GlyphTypefaceCache.TryGetValue("MiSans", out var glyphTypefaces));
Assert.Equal(3, glyphTypefaces.Count);
diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
index 5c830df4fa..9aae98efe9 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/FontCollectionTests.cs
@@ -18,33 +18,12 @@ namespace Avalonia.Skia.UnitTests.Media
private const string NotoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests";
- [InlineData("Hello World 6", "Hello World 6", FontStyle.Normal, FontWeight.Normal)]
- [InlineData("Hello World Italic", "Hello World", FontStyle.Italic, FontWeight.Normal)]
- [InlineData("Hello World Italic Bold", "Hello World", FontStyle.Italic, FontWeight.Bold)]
- [InlineData("FontAwesome 6 Free Regular", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Normal)]
- [InlineData("FontAwesome 6 Free Solid", "FontAwesome 6 Free", FontStyle.Normal, FontWeight.Solid)]
- [InlineData("FontAwesome 6 Brands", "FontAwesome 6 Brands", FontStyle.Normal, FontWeight.Normal)]
- [Theory]
- public void Should_Get_Implicit_Typeface(string input, string familyName, FontStyle style, FontWeight weight)
- {
- var typeface = new Typeface(input);
-
- var result = FontCollectionBase.GetImplicitTypeface(typeface, out var normalizedFamilyName);
-
- Assert.Equal(familyName, normalizedFamilyName);
- Assert.Equal(style, result.Style);
- Assert.Equal(weight, result.Weight);
- Assert.Equal(FontStretch.Normal, result.Stretch);
- }
-
[Win32Fact("Relies on some installed font family")]
public void Should_Cache_Nearest_Match()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
- var fontManager = FontManager.Current;
-
- var fontCollection = new TestSystemFontCollection(FontManager.Current);
+ var fontCollection = new TestSystemFontCollection(FontManager.Current.PlatformImpl);
Assert.True(fontCollection.TryGetGlyphTypeface("Arial", FontStyle.Normal, FontWeight.ExtraBlack, FontStretch.Normal, out var glyphTypeface));
@@ -62,9 +41,8 @@ namespace Avalonia.Skia.UnitTests.Media
private class TestSystemFontCollection : SystemFontCollection
{
- public TestSystemFontCollection(FontManager fontManager) : base(fontManager)
+ public TestSystemFontCollection(IFontManagerImpl platformImpl) : base(platformImpl)
{
-
}
public IDictionary> GlyphTypefaceCache => _glyphTypefaceCache;
@@ -81,8 +59,6 @@ namespace Avalonia.Skia.UnitTests.Media
var fontCollection = new CustomizableFontCollection(source, source, new[] { fallback });
- fontCollection.Initialize(FontManager.Current.PlatformImpl);
-
Assert.True(fontCollection.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var match));
Assert.Equal("Arial", match.FontFamily.Name);
@@ -100,8 +76,6 @@ namespace Avalonia.Skia.UnitTests.Media
var fontCollection = new CustomizableFontCollection(key, key, null, new[] { ignorable });
- fontCollection.Initialize(FontManager.Current.PlatformImpl);
-
var typeface = new Typeface(ignorable);
var glyphTypeface = typeface.GlyphTypeface;
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
index faacaa7d6d..3e2c12a912 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
@@ -6,6 +6,7 @@ using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Platform;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Xunit;
@@ -1352,8 +1353,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
.With(renderInterface: new PlatformRenderInterface(),
textShaperImpl: new TextShaperImpl()));
+ var customFontManagerImpl = new CustomFontManagerImpl();
+
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new FontManager(new CustomFontManagerImpl()));
+ .Bind().ToConstant(customFontManagerImpl);
+
+ var fontManager = new FontManager(customFontManagerImpl);
+
+ AvaloniaLocator.CurrentMutable
+ .Bind().ToConstant(fontManager);
+
+ fontManager.AddFontCollection(customFontManagerImpl.SystemFonts);
return disposable;
}
diff --git a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
index 24599c5a9b..7ca32e2d3b 100644
--- a/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
+++ b/tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
@@ -37,7 +37,7 @@ namespace Avalonia.UnitTests
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
- FontStretch fontStretch, CultureInfo culture, out Typeface fontKey)
+ FontStretch fontStretch, string? familyName, CultureInfo? culture, out Typeface fontKey)
{
foreach (var customTypeface in _customTypefaces)
{