Bladeren bron

Introduce FontManagerOptions (#7089)

* Introduce FontManagerOptions

* Add missing comments
Benedikt Stebner 3 jaren geleden
bovenliggende
commit
2633cf3ba4

+ 18 - 0
src/Avalonia.Visuals/Media/FontFallback.cs

@@ -0,0 +1,18 @@
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Font fallback definition that is used to override the default fallback lookup of the current <see cref="FontManager"/>
+    /// </summary>
+    public class FontFallback
+    {
+        /// <summary>
+        /// Get or set the fallback <see cref="FontFamily"/>
+        /// </summary>
+        public FontFamily FontFamily { get; set; }
+
+        /// <summary>
+        /// Get or set the <see cref="UnicodeRange"/> that is covered by the fallback.
+        /// </summary>
+        public UnicodeRange UnicodeRange { get; set; } = UnicodeRange.Default;
+    }
+}

+ 29 - 3
src/Avalonia.Visuals/Media/FontManager.cs

@@ -16,12 +16,17 @@ namespace Avalonia.Media
         private readonly ConcurrentDictionary<Typeface, GlyphTypeface> _glyphTypefaceCache =
             new ConcurrentDictionary<Typeface, GlyphTypeface>();
         private readonly FontFamily _defaultFontFamily;
+        private readonly IReadOnlyList<FontFallback> _fontFallbacks;
 
         public FontManager(IFontManagerImpl platformImpl)
         {
             PlatformImpl = platformImpl;
 
-            DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
+            var options = AvaloniaLocator.Current.GetService<FontManagerOptions>();
+
+            _fontFallbacks = options?.FontFallbacks;
+
+            DefaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName();
 
             if (string.IsNullOrEmpty(DefaultFontFamilyName))
             {
@@ -121,7 +126,28 @@ namespace Avalonia.Media
         /// </returns>
         public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
             FontWeight fontWeight,
-            FontFamily fontFamily, CultureInfo culture, out Typeface typeface) =>
-            PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface);
+            FontFamily fontFamily, CultureInfo culture, out Typeface typeface)
+        {
+            if(_fontFallbacks != null)
+            {
+                foreach (var fallback in _fontFallbacks)
+                {
+                    if(fallback is null)
+                    {
+                        continue;
+                    }
+
+                    typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight);
+
+                    var glyphTypeface = typeface.GlyphTypeface;
+
+                    if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
+                        return true;
+                    }
+                }
+            }
+
+            return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface);
+        }
     }
 }

+ 11 - 0
src/Avalonia.Visuals/Media/FontManagerOptions.cs

@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media
+{
+    public class FontManagerOptions
+    {
+        public string DefaultFamilyName { get; set; }
+
+        public IReadOnlyList<FontFallback> FontFallbacks { get; set; }
+    }
+}

+ 199 - 0
src/Avalonia.Visuals/Media/UnicodeRange.cs

@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// The <see cref="UnicodeRange"/> descripes a set of Unicode characters.
+    /// </summary>
+    public readonly struct UnicodeRange
+    {
+        public static UnicodeRange Default = Parse("0-10FFFD");
+
+        private readonly UnicodeRangeSegment _single;
+        private readonly IReadOnlyList<UnicodeRangeSegment> _segments = null;
+
+        public UnicodeRange(int start, int end)
+        {
+            _single = new UnicodeRangeSegment(start, end);
+        }
+
+        public UnicodeRange(UnicodeRangeSegment single)
+        {
+            _single = single;
+        }
+
+        public UnicodeRange(IReadOnlyList<UnicodeRangeSegment> segments)
+        {
+            if(segments is null || segments.Count == 0)
+            {
+                throw new ArgumentException(nameof(segments));
+            }
+
+            _single = segments[0];
+            _segments = segments;
+        }
+
+        internal UnicodeRangeSegment Single => _single;
+
+        internal IReadOnlyList<UnicodeRangeSegment> Segments => _segments;
+
+        /// <summary>
+        /// Determines if given value is inside the range.
+        /// </summary>
+        /// <param name="value">The value to verify.</param>
+        /// <returns>
+        /// <c>true</c> If given value is inside the range, <c>false</c> otherwise.
+        /// </returns>
+        public bool IsInRange(int value)
+        {
+            if(_segments is null)
+            {
+                return _single.IsInRange(value);
+            }
+
+            foreach(var segment in _segments)
+            {
+                if (segment.IsInRange(value))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Parses a <see cref="UnicodeRange"/>.
+        /// </summary>
+        /// <param name="s">The string to parse.</param>
+        /// <returns>The parsed <see cref="UnicodeRange"/>.</returns>
+        /// <exception cref="FormatException"></exception>
+        public static UnicodeRange Parse(string s)
+        {
+            if (string.IsNullOrEmpty(s))
+            {
+                throw new FormatException("Could not parse specified Unicode range.");
+            }
+
+            var parts = s.Split(',');
+
+            var length = parts.Length;
+
+            if(length == 0)
+            {
+                throw new FormatException("Could not parse specified Unicode range.");
+            }
+
+            if(length == 1)
+            {
+                return new UnicodeRange(UnicodeRangeSegment.Parse(parts[0]));
+            }
+
+            var segments = new UnicodeRangeSegment[length];
+
+            for (int i = 0; i < length; i++)
+            {
+                segments[i] = UnicodeRangeSegment.Parse(parts[i].Trim());
+            }
+
+            return new UnicodeRange(segments);
+        }
+    }
+
+    public readonly struct UnicodeRangeSegment
+    {
+        private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$");
+
+        public UnicodeRangeSegment(int start, int end)
+        {
+            Start = start;
+            End = end;
+        }
+
+        /// <summary>
+        /// Get the start of the segment.
+        /// </summary>
+        public int Start { get; }
+
+        /// <summary>
+        /// Get the end of the segment.
+        /// </summary>
+        public int End { get; }
+
+        /// <summary>
+        /// Determines if given value is inside the range segment.
+        /// </summary>
+        /// <param name="value">The value to verify.</param>
+        /// <returns>
+        /// <c>true</c> If given value is inside the range segment, <c>false</c> otherwise.
+        /// </returns>
+        public bool IsInRange(int value)
+        {
+            return value - Start <= End - Start;
+        }
+
+        /// <summary>
+        /// Parses a <see cref="UnicodeRangeSegment"/>.
+        /// </summary>
+        /// <param name="s">The string to parse.</param>
+        /// <returns>The parsed <see cref="UnicodeRangeSegment"/>.</returns>
+        /// <exception cref="FormatException"></exception>
+        public static UnicodeRangeSegment Parse(string s)
+        {
+            if (string.IsNullOrEmpty(s))
+            {
+                throw new FormatException("Could not parse specified Unicode range segment.");
+            }
+
+            var parts = s.Split('-');
+
+            int start, end;
+
+            switch (parts.Length)
+            {
+                case 1:
+                    {
+                        //e.g. U+20, U+3F U+30??
+                        var single = s_regex.Match(parts[0]);
+
+                        if (!single.Success)
+                        {
+                            throw new FormatException("Could not parse specified Unicode range segment.");
+                        }
+
+                        if (!single.Value.Contains("?"))
+                        {
+                            start = int.Parse(single.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
+                            end = start;
+                        }
+                        else
+                        {
+                            start = int.Parse(single.Groups[1].Value.Replace('?', '0'), System.Globalization.NumberStyles.HexNumber);
+                            end = int.Parse(single.Groups[1].Value.Replace('?', 'F'), System.Globalization.NumberStyles.HexNumber);
+                        }
+                        break;
+                    }
+                case 2:
+                    {
+                        var first = s_regex.Match(parts[0]);
+                        var second = s_regex.Match(parts[1]);
+
+                        if (!first.Success || !second.Success)
+                        {
+                            throw new FormatException("Could not parse specified Unicode range segment.");
+                        }
+
+                        start = int.Parse(first.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
+                        end = int.Parse(second.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
+                        break;
+                    }
+                default:
+                    throw new FormatException("Could not parse specified Unicode range segment.");
+            }
+
+            return new UnicodeRangeSegment(start, end);
+        }
+    }
+}

BIN
src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf


BIN
src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf


+ 0 - 1
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@@ -34,7 +34,6 @@
   <Import Project="..\..\..\build\HarfBuzzSharp.props" />
 
   <ItemGroup>
-    <AvaloniaResource Include="Assets\*" />
     <Content Include="*.props">
       <Pack>true</Pack>
       <PackagePath>build\</PackagePath>

+ 0 - 2
src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs

@@ -25,8 +25,6 @@ namespace Avalonia.Web.Blazor
                 .UseSkia()
                 .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() });
 
-            AvaloniaLocator.CurrentMutable.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));
-
             return builder;
         }
     }

+ 0 - 78
src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs

@@ -1,78 +0,0 @@
-using System.Globalization;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Skia;
-using SkiaSharp;
-
-namespace Avalonia.Web.Blazor
-{
-    public class CustomFontManagerImpl : IFontManagerImpl
-    {
-        private readonly Typeface[] _customTypefaces;
-        private readonly string _defaultFamilyName;
-
-        private readonly Typeface _defaultTypeface =
-            new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Mono");
-        private readonly Typeface _italicTypeface =
-            new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Sans");
-
-        public CustomFontManagerImpl()
-        {
-            _customTypefaces = new[] { _italicTypeface, _defaultTypeface };
-            _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
-        }
-
-        public string GetDefaultFontFamilyName()
-        {
-            return _defaultFamilyName;
-        }
-
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
-        {
-            return _customTypefaces.Select(x => x.FontFamily.Name);
-        }
-
-        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
-            CultureInfo culture, out Typeface typeface)
-        {
-            foreach (var customTypeface in _customTypefaces)
-            {
-                if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
-                {
-                    continue;
-                }
-
-                typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight);
-
-                return true;
-            }
-
-            typeface = _defaultTypeface;
-
-            return true;
-        }
-
-        public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
-        {
-            SKTypeface skTypeface;
-
-            switch (typeface.FontFamily.Name)
-            {
-                case "Noto Sans":
-                    {
-                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
-                        skTypeface = typefaceCollection.Get(typeface);
-                        break;
-                    }
-                default:
-                    {
-                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
-                        skTypeface = typefaceCollection.Get(_defaultTypeface);
-                        break;
-                    }
-            }
-
-            return new GlyphTypefaceImpl(skTypeface);
-        }
-    }
-}

+ 1 - 1
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@@ -17,7 +17,7 @@ namespace Avalonia.UnitTests
 
         public ushort GetGlyph(uint codepoint)
         {
-            return 0;
+            return (ushort)codepoint;
         }
 
         public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)

+ 30 - 0
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@@ -30,5 +30,35 @@ namespace Avalonia.Visuals.UnitTests.Media
                 Assert.Throws<InvalidOperationException>(() => FontManager.Current);
             }
         }
+
+        [Fact]
+        public void Should_Use_FontManagerOptions_DefaultFamilyName()
+        {
+            var options = new FontManagerOptions { DefaultFamilyName = "MyFont" };
+
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
+                .With(fontManagerImpl: new MockFontManagerImpl())))
+            {
+                AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options);
+
+                Assert.Equal("MyFont", FontManager.Current.DefaultFontFamilyName);
+            }
+        }
+
+        [Fact]
+        public void Should_Use_FontManagerOptions_FontFallback()
+        {
+            var options = new FontManagerOptions { FontFallbacks = new[] { new FontFallback { FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default} } };
+
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
+                .With(fontManagerImpl: new MockFontManagerImpl())))
+            {
+                AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options);
+
+                FontManager.Current.TryMatchCharacter(1, FontStyle.Normal, FontWeight.Normal, FontFamily.Default, null, out var typeface);
+
+                Assert.Equal("MyFont", typeface.FontFamily.Name);
+            }
+        }
     }
 }

+ 22 - 0
tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeSegmentTests.cs

@@ -0,0 +1,22 @@
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class UnicodeRangeSegmentTests
+    {
+        [InlineData("u+00-FF", 0, 255)]
+        [InlineData("U+00-FF", 0, 255)]
+        [InlineData("U+00-U+FF", 0, 255)]
+        [InlineData("U+AB??", 43776, 44031)]
+        [Theory]
+        public void Should_Parse(string s, int expectedStart, int expectedEnd)
+        {
+            var segment = UnicodeRangeSegment.Parse(s);
+
+            Assert.Equal(expectedStart, segment.Start);
+
+            Assert.Equal(expectedEnd, segment.End);
+        }
+    }
+}

+ 17 - 0
tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeTests.cs

@@ -0,0 +1,17 @@
+using System.Linq;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class UnicodeRangeTests
+    {
+        [Fact]
+        public void Should_Parse_Segments()
+        {
+            var range = UnicodeRange.Parse("U+0, U+1, U+2, U+3");
+
+            Assert.Equal(new[] { 0, 1, 2, 3 }, range.Segments.Select(x => x.Start));
+        }
+    }
+}