diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 92f41c6606..146d6df001 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -1,4 +1,4 @@
-
+
@@ -1375,6 +1375,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@)
+ 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)
@@ -2773,6 +2779,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Media.Fonts.FontCollectionBase.TryGetGlyphTypeface(System.String,Avalonia.Media.Fonts.FontCollectionKey,Avalonia.Media.GlyphTypeface@)
+ 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)
@@ -4969,4 +4981,4 @@
baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
index 40176c88ff..6e4283d76e 100644
--- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
+++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
@@ -168,7 +168,7 @@ namespace Avalonia.Media.Fonts
var key = typeface.ToFontCollectionKey();
- return TryGetGlyphTypeface(familyName, key, out glyphTypeface);
+ return TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface);
}
public virtual bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces)
@@ -455,25 +455,25 @@ namespace Avalonia.Media.Fonts
/// 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.
+ /// Whether to allow a nearest match (as opposed to only an exact match).
/// 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 GlyphTypeface? glyphTypeface)
+ protected bool TryGetGlyphTypeface(
+ string familyName,
+ FontCollectionKey key,
+ bool allowNearestMatch,
+ [NotNullWhen(true)] out GlyphTypeface? 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))
+ if (TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out var isNearestMatch))
{
var matchedKey = glyphTypeface.ToFontCollectionKey();
- if (matchedKey != key)
+ if (isNearestMatch && matchedKey != key)
{
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, key.Style, key.Weight, key.Stretch, out var syntheticGlyphTypeface))
{
@@ -511,7 +511,7 @@ namespace Avalonia.Media.Fonts
{
// 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))
+ TryGetMatch(exactGlyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _))
{
return true;
}
@@ -549,7 +549,7 @@ namespace Avalonia.Media.Fonts
}
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
- TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+ TryGetMatch(glyphTypefaces, key, allowNearestMatch, out glyphTypeface, out _))
{
return true;
}
@@ -559,6 +559,29 @@ namespace Avalonia.Media.Fonts
return false;
}
+ private bool TryGetMatch(
+ IDictionary glyphTypefaces,
+ FontCollectionKey key,
+ bool allowNearestMatch,
+ [NotNullWhen(true)] out GlyphTypeface? glyphTypeface,
+ out bool isNearestMatch)
+ {
+ if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface is not null)
+ {
+ isNearestMatch = false;
+ return true;
+ }
+
+ if (allowNearestMatch && TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+ {
+ isNearestMatch = true;
+ return true;
+ }
+
+ isNearestMatch = false;
+ return false;
+ }
+
///
/// Attempts to retrieve the nearest matching for the specified font key from the
/// provided collection of glyph typefaces.
diff --git a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
index 3c81e9890f..1c79127ec3 100644
--- a/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
+++ b/src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
@@ -29,14 +29,14 @@ namespace Avalonia.Media.Fonts
FontStretch stretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface)
{
var typeface = new Typeface(familyName, style, weight, stretch).Normalize(out familyName);
+ var key = typeface.ToFontCollectionKey();
- if (base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
+ // Find an exact match first
+ if (TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface))
{
return true;
}
- var key = typeface.ToFontCollectionKey();
-
//Check cache first to avoid unnecessary calls to the font manager
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces) && glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
@@ -52,6 +52,13 @@ namespace Avalonia.Media.Fonts
return false;
}
+ // The font manager didn't return a perfect match either. Find the nearest match ourselves.
+ if (key != platformTypeface.ToFontCollectionKey() &&
+ TryGetGlyphTypeface(familyName, key, allowNearestMatch: true, out glyphTypeface))
+ {
+ return true;
+ }
+
glyphTypeface = GlyphTypeface.TryCreate(platformTypeface);
if (glyphTypeface is null)
{
@@ -77,7 +84,7 @@ namespace Avalonia.Media.Fonts
}
//Requested glyph typeface should be in cache now
- return base.TryGetGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface);
+ return TryGetGlyphTypeface(familyName, key, allowNearestMatch: false, out glyphTypeface);
}
public override bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList? familyTypefaces)
diff --git a/tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf b/tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf
new file mode 100644
index 0000000000..8e82c70d10
Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/Inter-Bold.ttf differ
diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
index a66292b880..989b8c5824 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
@@ -29,20 +29,21 @@ namespace Avalonia.Skia.UnitTests.Media
var infos = new[]
{
- new FontAssetInfo($"{AssetsNamespace}.AdobeBlank2VF.ttf", "Adobe Blank 2 VF R"),
- new FontAssetInfo($"{AssetsNamespace}.Inter-Regular.ttf", "Inter"),
- new FontAssetInfo($"{AssetsNamespace}.Manrope-Light.ttf", "Manrope Light"),
- new FontAssetInfo($"{AssetsNamespace}.MiSans-Normal.ttf", "MiSans Normal"),
- new FontAssetInfo($"{AssetsNamespace}.NISC18030.ttf", "GB18030 Bitmap"),
- new FontAssetInfo($"{AssetsNamespace}.NotoMono-Regular.ttf", "Noto Mono"),
- new FontAssetInfo($"{AssetsNamespace}.NotoSans-Italic.ttf", "Noto Sans"),
- new FontAssetInfo($"{AssetsNamespace}.NotoSansArabic-Regular.ttf", "Noto Sans Arabic"),
- new FontAssetInfo($"{AssetsNamespace}.NotoSansDeseret-Regular.ttf", "Noto Sans Deseret"),
- new FontAssetInfo($"{AssetsNamespace}.NotoSansHebrew-Regular.ttf", "Noto Sans Hebrew"),
- new FontAssetInfo($"{AssetsNamespace}.NotoSansMiao-Regular.ttf", "Noto Sans Miao"),
- new FontAssetInfo($"{AssetsNamespace}.NotoSansTamil-Regular.ttf", "Noto Sans Tamil"),
- new FontAssetInfo($"{AssetsNamespace}.SourceSerif4_36pt-Italic.ttf", "Source Serif 4 36pt"),
- new FontAssetInfo($"{AssetsNamespace}.TwitterColorEmoji-SVGinOT.ttf", "Twitter Color Emoji")
+ new FontAssetInfo($"{AssetsNamespace}.AdobeBlank2VF.ttf", "Adobe Blank 2 VF R", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.Inter-Bold.ttf", "Inter", FontWeight.Bold),
+ new FontAssetInfo($"{AssetsNamespace}.Inter-Regular.ttf", "Inter", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.Manrope-Light.ttf", "Manrope Light", FontWeight.Light),
+ new FontAssetInfo($"{AssetsNamespace}.MiSans-Normal.ttf", "MiSans Normal", (FontWeight)305),
+ new FontAssetInfo($"{AssetsNamespace}.NISC18030.ttf", "GB18030 Bitmap", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.NotoMono-Regular.ttf", "Noto Mono", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.NotoSans-Italic.ttf", "Noto Sans", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.NotoSansArabic-Regular.ttf", "Noto Sans Arabic", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.NotoSansDeseret-Regular.ttf", "Noto Sans Deseret", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.NotoSansHebrew-Regular.ttf", "Noto Sans Hebrew", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.NotoSansMiao-Regular.ttf", "Noto Sans Miao", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.NotoSansTamil-Regular.ttf", "Noto Sans Tamil", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.SourceSerif4_36pt-Italic.ttf", "Source Serif 4 36pt", FontWeight.Normal),
+ new FontAssetInfo($"{AssetsNamespace}.TwitterColorEmoji-SVGinOT.ttf", "Twitter Color Emoji", FontWeight.Normal)
};
var assets = assetLoader.GetAssets(new Uri(AssetFonts, UriKind.Absolute), null)
@@ -51,6 +52,9 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Equal(infos.Length, assets.Length);
+ var glyphTypefaces = new GlyphTypeface[infos.Length];
+
+ // Load fonts
for (var i = 0; i < infos.Length; ++i)
{
var info = infos[i];
@@ -63,8 +67,18 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.True(fontCollection.TryAddGlyphTypeface(fontStream, out var glyphTypeface));
Assert.Equal(info.FamilyName, glyphTypeface.FamilyName);
+ Assert.Equal(info.Weight, glyphTypeface.Weight);
+
+ glyphTypefaces[i] = glyphTypeface;
+ }
+
+ // Check against the custom collection
+ for (var i = 0; i < infos.Length; ++i)
+ {
+ var info = infos[i];
+ var glyphTypeface = glyphTypefaces[i];
- Assert.True(fontManager.TryGetGlyphTypeface(new Typeface($"fonts:custom#{info.FamilyName}"), out var secondGlyphTypeface));
+ Assert.True(fontManager.TryGetGlyphTypeface(new Typeface($"fonts:custom#{info.FamilyName}", weight: info.Weight), out var secondGlyphTypeface));
Assert.Same(glyphTypeface, secondGlyphTypeface);
}
}
@@ -207,6 +221,6 @@ namespace Avalonia.Skia.UnitTests.Media
public override Uri Key { get; } = key;
}
- private record struct FontAssetInfo(string Path, string FamilyName);
+ private record struct FontAssetInfo(string Path, string FamilyName, FontWeight Weight);
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
index 457598a88c..b0d6e1bfd1 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
@@ -619,5 +619,29 @@ namespace Avalonia.Skia.UnitTests.Media
Assert.Equal("Inter", typeface.GlyphTypeface.FamilyName);
Assert.Equal(requestedStretch, typeface.Stretch);
}
+
+ [Fact]
+ public void TryGetGlyphTypeface_Should_Use_Perfect_Match_In_Collection_Before_Nearest_Match()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new CustomFontManagerImpl()));
+ using var scope = AvaloniaLocator.EnterScope();
+
+ // Load bold font (Inter-Bold.ttf) first
+ Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, FontWeight.Bold), out var boldGlyphTypeface));
+ Assert.NotNull(boldGlyphTypeface);
+ Assert.Equal("Inter", boldGlyphTypeface.FamilyName);
+ Assert.Equal(FontWeight.Bold, boldGlyphTypeface.Weight);
+
+ // Normal font (Inter-Regular.ttf) should be loaded since it's a perfect match, instead of falling back
+ Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, FontWeight.Normal), out var regularGlyphTypeface));
+ Assert.NotNull(regularGlyphTypeface);
+ Assert.NotSame(regularGlyphTypeface, boldGlyphTypeface);
+ Assert.Equal("Inter", regularGlyphTypeface.FamilyName);
+ Assert.Equal(FontWeight.Normal, regularGlyphTypeface.Weight);
+
+ // Nearest match should still work (650 falls back to 700 Bold)
+ Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("Inter", FontStyle.Normal, (FontWeight)650), out var nearestMatchTypeface));
+ Assert.Same(boldGlyphTypeface, nearestMatchTypeface);
+ }
}
}