File: Fonts\FontManager.iOS.cs
Web Access
Project: src\src\Core\src\Core.csproj (Microsoft.Maui)
using System;
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using UIKit;
 
namespace Microsoft.Maui
{
	/// <inheritdoc/>
	public class FontManager : IFontManager
	{
		// UIFontWeight[Constant] is internal in Xamarin.iOS but the convertion from
		// the public (int-based) enum is not helpful in this case.
		// -1.0 (Thin / 100) to 1.0 (Black / 900) with 0 being Regular (400)
		// which is not quite the center, not are the constant values linear
		static readonly (float value, FontWeight weight)[] FontWeightMap = new (float, FontWeight)[] {
			(-0.80f, FontWeight.Ultralight),
			(-0.60f, FontWeight.Thin),
			(-0.40f, FontWeight.Light),
			(0.0f, FontWeight.Regular),
			(0.23f, FontWeight.Medium),
			(0.30f, FontWeight.Semibold),
			(0.40f, FontWeight.Bold),
			(0.56f, FontWeight.Heavy),
			(0.62f, FontWeight.Black)
		};
 
		readonly ConcurrentDictionary<Font, UIFont> _fonts = new();
		readonly IFontRegistrar _fontRegistrar;
		readonly IServiceProvider? _serviceProvider;
 
		UIFont? _defaultFont;
 
		/// <summary>
		/// Creates a new <see cref="EmbeddedFontLoader"/> instance.
		/// </summary>
		/// <param name="fontRegistrar">A <see cref="IFontRegistrar"/> instance to retrieve details from about registered fonts.</param>
		/// <param name="serviceProvider">The applications <see cref="IServiceProvider"/>.
		/// Typically this is provided through dependency injection.</param>
		public FontManager(IFontRegistrar fontRegistrar, IServiceProvider? serviceProvider = null)
		{
			_fontRegistrar = fontRegistrar;
			_serviceProvider = serviceProvider;
		}
 
		/// <inheritdoc/>
		public UIFont DefaultFont =>
			_defaultFont ??= UIFont.SystemFontOfSize(UIFont.SystemFontSize);
 
		static double? defaultFontSize;
 
		/// <inheritdoc/>
		public double DefaultFontSize => defaultFontSize ??= UIFont.SystemFontSize;
 
		/// <inheritdoc/>
		public UIFont GetFont(Font font, double defaultFontSize = 0) =>
			GetFont(font, defaultFontSize, CreateFont);
 
		double GetFontSize(Font font, double defaultFontSize = 0) =>
			font.Size <= 0 || double.IsNaN(font.Size)
				? (defaultFontSize > 0 ? (float)defaultFontSize : DefaultFont.PointSize)
				: (nfloat)font.Size;
 
		static float GetWeightConstant(FontWeight self)
		{
			foreach (var (value, weight) in FontWeightMap)
			{
				if (self <= weight)
					return value;
			}
			return 1.0f;
		}
 
		UIFont GetFont(Font font, double defaultFont, Func<Font, UIFont> factory)
		{
			var size = GetFontSize(font, defaultFont);
			if (size != font.Size)
				font = font.WithSize(size);
			return _fonts.GetOrAdd(font, factory);
		}
 
		static UIFontAttributes GetFontAttributes(Font font)
		{
			var a = new UIFontAttributes
			{
				Traits = new UIFontTraits(),
			};
			var weight = font.Weight;
			if (font.Weight == 0)
				weight = FontWeight.Regular;
			var traits = (UIFontDescriptorSymbolicTraits)0;
			if (weight == FontWeight.Bold)
				traits |= UIFontDescriptorSymbolicTraits.Bold;
			else if (weight != FontWeight.Regular)
			{
				a.Traits = new UIFontTraits
				{
					Weight = GetWeightConstant(font.Weight),
					Slant = font.Slant == FontSlant.Oblique ? 30.0f : 0.0f
				};
			}
			if (font.Slant == FontSlant.Italic)
				traits |= UIFontDescriptorSymbolicTraits.Italic;
 
			a.Traits.SymbolicTrait = traits;
			return a;
		}
 
		UIFont CreateFont(Font font)
		{
			var family = font.Family;
			var size = (nfloat)font.Size;
 
			var hasAttributes =
				font.Weight != FontWeight.Regular ||
				font.Slant != FontSlant.Default;
 
			if (family != null && family != DefaultFont.FamilyName)
			{
				try
				{
					UIFont? result = null;
 
					if (Array.IndexOf(UIFont.FamilyNames, family) != -1)
					{
						var descriptor = new UIFontDescriptor().CreateWithFamily(family);
						if (hasAttributes)
							descriptor = descriptor.CreateWithAttributes(GetFontAttributes(font));
 
						result = UIFont.FromDescriptor(descriptor, size);
						if (result != null)
							return ApplyScaling(font, result);
					}
 
					if (family.StartsWith(".SFUI", StringComparison.InvariantCultureIgnoreCase))
					{
						var weights = family.Split('-');
						var fontWeight = weights.Length == 0
							? null
							: weights[weights.Length - 1];
 
						if (!string.IsNullOrWhiteSpace(fontWeight) && Enum.TryParse<UIFontWeight>(fontWeight, true, out var uIFontWeight))
						{
							result = UIFont.SystemFontOfSize(size, uIFontWeight);
							if (result != null)
								return ApplyScaling(font, result);
						}
 
						result = UIFont.SystemFontOfSize(size, UIFontWeight.Regular);
						if (result != null)
							return ApplyScaling(font, result);
					}
 
					var cleansedFont = CleanseFontName(family);
					result = UIFont.FromName(cleansedFont, size);
					if (result != null)
						return ApplyScaling(font, result);
 
					result = UIFont.FromName(family, size);
					if (result != null)
						return ApplyScaling(font, result);
				}
				catch (Exception ex)
				{
					_serviceProvider?.CreateLogger<FontManager>()?.LogWarning(ex, "Unable to load font '{Font}'.", family);
				}
			}
 
			if (hasAttributes)
			{
				var defaultFont = UIFont.SystemFontOfSize(size);
				var descriptor = defaultFont.FontDescriptor.CreateWithAttributes(GetFontAttributes(font));
				return ApplyScaling(font, UIFont.FromDescriptor(descriptor, size));
			}
 
			return ApplyScaling(font, UIFont.SystemFontOfSize(size));
 
			UIFont ApplyScaling(Font font, UIFont uiFont)
			{
				if (font.AutoScalingEnabled && (OperatingSystem.IsIOSVersionAtLeast(11) || OperatingSystem.IsMacCatalystVersionAtLeast(11)))
					return UIFontMetrics.DefaultMetrics.GetScaledFont(uiFont);
 
				return uiFont;
			}
		}
 
		string? CleanseFontName(string fontName)
		{
			// First check Alias
			if (_fontRegistrar.GetFont(fontName) is string fontPostScriptName)
				return fontPostScriptName;
 
			var fontFile = FontFile.FromString(fontName);
 
			if (!string.IsNullOrWhiteSpace(fontFile.Extension))
			{
				if (_fontRegistrar.GetFont(fontFile.FileNameWithExtension()) is string filePath)
					return filePath ?? fontFile.PostScriptName;
			}
			else
			{
				foreach (var ext in FontFile.Extensions)
				{
					var formatted = fontFile.FileNameWithExtension(ext);
					if (_fontRegistrar.GetFont(formatted) is string filePath)
						return filePath;
				}
			}
 
			return fontFile.PostScriptName;
		}
	}
}