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); + } } }