Skip to content

Improve FontCollection customization#19756

Merged
MrJul merged 10 commits into
AvaloniaUI:masterfrom
Gillibald:feature/publicFontCollectionHelper
Nov 24, 2025
Merged

Improve FontCollection customization#19756
MrJul merged 10 commits into
AvaloniaUI:masterfrom
Gillibald:feature/publicFontCollectionHelper

Conversation

@Gillibald

@Gillibald Gillibald commented Oct 3, 2025

Copy link
Copy Markdown
Contributor

What does the pull request do?

This change enhances font collection matching, loading and synthetic glyph typeface creation. It improves nearest-match logic for style/weight/stretch, adds prefix-family lookup when an exact family is missing, better caches fallback matches and supports loading fonts from resource and file sources (including directories). Several platform font manager and glyph typeface implementations and unit tests were updated to align with the new behavior.

Key highlights

  • Improved matching:
    • Nearest-match fallback for style, weight and stretch.
    • Prefix family name search when exact family not found.
    • Cache nearest matches and attempt synthetic glyph typeface creation when needed.
  • Synthetic typefaces:
    • Attempt to synthesize bold/oblique via TryCreateSyntheticGlyphTypeface when exact-match missing.
    • Add synthesized entries to cache under typographic and family names where available.
  • Loading:
    • TryAddFontSource supports avares / resm resource schemes and file scheme for files and directories.
    • Stream-based TryAddGlyphTypeface helper added.
  • Threading and collection publishing:
    • AddFontFamily maintains a sorted snapshot array published atomically for readers.
  • Tests:
    • Unit tests updated/added to cover matching, font loading, synthetic typeface creation and text formatting behavior.

Tests

  • Existing unit tests updated to cover new nearest match and loading behavior.
  • New/updated mocks for platform font managers to validate synthetic glyph creation and stream based loading.

Notes & migration

  • Consumers relying on previous familyName exact lookup may observe improved fallback behavior (prefix matching and synthetic typefaces).
  • Platform font manager implementations were adjusted. Ensure any custom IFontManagerImpl implementations are compatible with the updated TryCreateGlyphTypeface and IGlyphTypeface2 usage.

What is the current behavior?

Example for TryAddFontSource

class CustomFontCollection(Uri key) : FontCollectionBase
{
    public override Uri Key { get; } = key;
}
var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute));
fontManager.AddFontCollection(fontCollection);
var source = new Uri("my/path/to/source", UriKind.Absolute);
Assert.True(fontCollection.TryAddFontSource(fontUri));

How was the solution implemented (if it's not obvious)?

Checklist

Breaking changes

Obsoletions / Deprecations

Fixed issues

@Gillibald Gillibald added enhancement backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch labels Oct 3, 2025
maxkatz6
maxkatz6 previously approved these changes Oct 3, 2025
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059162-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Comment thread src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs Outdated
Comment thread src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs Outdated
@Gillibald Gillibald changed the title Make TryGetNearestMatch and GetImplicitTypeface public [WIP] Make TryGetNearestMatch and GetImplicitTypeface public Oct 3, 2025
@Gillibald Gillibald force-pushed the feature/publicFontCollectionHelper branch from 85b525f to c06c943 Compare October 6, 2025 08:13
@Gillibald Gillibald changed the title [WIP] Make TryGetNearestMatch and GetImplicitTypeface public [WIP] Introduce CustomFontCollection Oct 6, 2025
@Gillibald Gillibald added the needs-api-review The PR adds new public APIs that should be reviewed. label Oct 6, 2025
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059223-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Gillibald Gillibald changed the title [WIP] Introduce CustomFontCollection Introduce CustomFontCollection Oct 6, 2025
Comment thread src/Avalonia.Base/Media/Fonts/CustomFontCollection.cs Outdated
Comment thread src/Avalonia.Base/Media/Fonts/CustomFontCollection.cs Outdated
@miloush

miloush commented Oct 6, 2025

Copy link
Copy Markdown
Contributor

It might also be useful to add whole font collection into another

@Gillibald

Copy link
Copy Markdown
Contributor Author

A copy can be done via:

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

But we can also introduce some API to make this easier

@miloush

miloush commented Oct 6, 2025

Copy link
Copy Markdown
Contributor

I was thinking of IDWriteFontSetBuilder.AddFontSet equivalent but the collections don't have that anyway.

Comment thread src/Avalonia.Base/Media/Fonts/CustomFontCollection.cs Outdated
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059227-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Gillibald Gillibald changed the title Introduce CustomFontCollection Improve FontCollection customization Oct 8, 2025
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059256-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Gillibald Gillibald changed the title Improve FontCollection customization [WIP] Improve FontCollection customization Oct 9, 2025
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059270-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Gillibald Gillibald changed the title [WIP] Improve FontCollection customization Improve FontCollection customization Oct 9, 2025
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059276-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059356-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0059439-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul MrJul removed the backport-candidate-11.3.x Consider this PR for backporting to 11.3 branch label Nov 12, 2025
@MrJul MrJul added api-change-requested The new public APIs need some changes. and removed needs-api-review The PR adds new public APIs that should be reviewed. labels Nov 12, 2025
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0060020-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@Gillibald

Gillibald commented Nov 14, 2025

Copy link
Copy Markdown
Contributor Author

API diff between 12.0.999-cibuild0060140-alpha and 12.0.999

Avalonia.Base (net6.0, net8.0, netstandard2.0)

  namespace Avalonia.Media
  {
      public sealed class Typeface
      {
+         public Avalonia.Media.Typeface Normalize(out string normalizedFamilyName);
      }
  }
  namespace Avalonia.Media.Fonts
  {
      public class EmbeddedFontCollection : Avalonia.Media.Fonts.FontCollectionBase
      {
-         public override System.Collections.Generic.IEnumerator<Avalonia.Media.FontFamily> GetEnumerator();
-         public override void Initialize(Avalonia.Platform.IFontManagerImpl fontManager);
-         public bool TryGetFamilyTypefaces(string familyName, out System.Collections.Generic.IReadOnlyList<Avalonia.Media.Typeface?>? familyTypefaces);
-         public override bool TryGetGlyphTypeface(string familyName, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
-         public override int Count { get; }
-         public override Avalonia.Media.FontFamily this[int index] {
-             get { }
-         }
      }
      public abstract class FontCollectionBase : Avalonia.Media.Fonts.IFontCollection
      {
-         protected readonly System.Collections.Concurrent.ConcurrentDictionary<string, System.Collections.Concurrent.ConcurrentDictionary<Avalonia.Media.Fonts.FontCollectionKey, Avalonia.Media.IGlyphTypeface>> _glyphTypefaceCache;
+         public System.Collections.Generic.IEnumerator<Avalonia.Media.FontFamily> GetEnumerator();
-         public abstract System.Collections.Generic.IEnumerator<Avalonia.Media.FontFamily> GetEnumerator();
-         public abstract void Initialize(Avalonia.Platform.IFontManagerImpl fontManager);
-         public abstract bool TryGetGlyphTypeface(string familyName, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
+         public virtual bool TryGetGlyphTypeface(string familyName, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
-         public abstract int Count { get; }
+         public int Count { get; }
-         public abstract Avalonia.Media.FontFamily this[int index] { get; }
+         public Avalonia.Media.FontFamily this[int index] {
+             get { }
+         }
+         protected void AddFontFamily(Avalonia.Media.FontFamily fontFamily);
+         public bool TryAddFontSource(System.Uri source);
+         public bool TryAddGlyphTypeface(Avalonia.Media.IGlyphTypeface glyphTypeface);
+         public bool TryAddGlyphTypeface(System.IO.Stream stream, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
+         protected bool TryAddGlyphTypeface(string familyName, Avalonia.Media.Fonts.FontCollectionKey key, Avalonia.Media.IGlyphTypeface? glyphTypeface);
+         public virtual bool TryGetFamilyTypefaces(string familyName, out System.Collections.Generic.IReadOnlyList<Avalonia.Media.Typeface?>? familyTypefaces);
+         protected bool TryGetGlyphTypeface(string familyName, Avalonia.Media.Fonts.FontCollectionKey key, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
+         protected bool? TryGetNearestMatch(System.Collections.Generic.IDictionary<Avalonia.Media.Fonts.FontCollectionKey, Avalonia.Media.IGlyphTypeface> glyphTypefaces, Avalonia.Media.Fonts.FontCollectionKey? key, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
+         public bool TryGetNearestMatch(string familyName, Avalonia.Media.FontStyle style, Avalonia.Media.FontWeight weight, Avalonia.Media.FontStretch stretch, out Avalonia.Media.IGlyphTypeface? glyphTypeface);
      }
-     public static class FontFamilyLoader
-     {
-         public static System.Collections.Generic.IEnumerable<System.Uri> LoadFontAssets(System.Uri source);
-     }
      public interface IFontCollection
      {
-         void Initialize(Avalonia.Platform.IFontManagerImpl fontManager);
      }
  }
  namespace Avalonia.Platform
  {
      public interface IFontManagerImpl
      {
-         bool? TryMatchCharacter(int? codepoint, Avalonia.Media.FontStyle? fontStyle, Avalonia.Media.FontWeight? fontWeight, Avalonia.Media.FontStretch? fontStretch, System.Globalization.CultureInfo? culture, out Avalonia.Media.Typeface? typeface);
+         bool? TryMatchCharacter(int? codepoint, Avalonia.Media.FontStyle? fontStyle, Avalonia.Media.FontWeight? fontWeight, Avalonia.Media.FontStretch? fontStretch, string? familyName, System.Globalization.CultureInfo? culture, out Avalonia.Media.Typeface? typeface);
      }
  }

@Gillibald Gillibald force-pushed the feature/publicFontCollectionHelper branch from 5af34bd to 7c6637e Compare November 14, 2025 10:27
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0060022-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0060110-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

Comment thread src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs Outdated
Comment thread src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs Outdated
Comment thread src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs Outdated
Comment thread src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs Outdated
Comment thread src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs Outdated
Comment thread src/Avalonia.Base/Media/FontManager.cs Outdated
Comment thread src/Avalonia.Base/Media/FontManager.cs Outdated
Comment thread src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs Outdated
Comment thread src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs Outdated
Comment thread tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0060144-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0060148-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

…nHelper

# Conflicts:
#	api/Avalonia.nupkg.xml
#	src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
#	tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@MrJul MrJul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@MrJul MrJul enabled auto-merge November 24, 2025 15:36
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.0.999-cibuild0060307-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul MrJul added this pull request to the merge queue Nov 24, 2025
Merged via the queue into AvaloniaUI:master with commit 3e62621 Nov 24, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-change-requested The new public APIs need some changes. breaking-change enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants