diff --git a/src/Avalonia.Visuals/Media/FontFallback.cs b/src/Avalonia.Visuals/Media/FontFallback.cs new file mode 100644 index 0000000000..240604c5c1 --- /dev/null +++ b/src/Avalonia.Visuals/Media/FontFallback.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Media +{ + /// + /// Font fallback definition that is used to override the default fallback lookup of the current + /// + public class FontFallback + { + /// + /// Get or set the fallback + /// + public FontFamily FontFamily { get; set; } + + /// + /// Get or set the that is covered by the fallback. + /// + public UnicodeRange UnicodeRange { get; set; } = UnicodeRange.Default; + } +} diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index db87c7c6c4..33e7bf0200 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -16,12 +16,17 @@ namespace Avalonia.Media private readonly ConcurrentDictionary _glyphTypefaceCache = new ConcurrentDictionary(); private readonly FontFamily _defaultFontFamily; + private readonly IReadOnlyList _fontFallbacks; public FontManager(IFontManagerImpl platformImpl) { PlatformImpl = platformImpl; - DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + var options = AvaloniaLocator.Current.GetService(); + + _fontFallbacks = options?.FontFallbacks; + + DefaultFontFamilyName = options?.DefaultFamilyName ?? PlatformImpl.GetDefaultFontFamilyName(); if (string.IsNullOrEmpty(DefaultFontFamilyName)) { @@ -121,7 +126,28 @@ namespace Avalonia.Media /// 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); + } } } diff --git a/src/Avalonia.Visuals/Media/FontManagerOptions.cs b/src/Avalonia.Visuals/Media/FontManagerOptions.cs new file mode 100644 index 0000000000..983fd6eb32 --- /dev/null +++ b/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 FontFallbacks { get; set; } + } +} diff --git a/src/Avalonia.Visuals/Media/UnicodeRange.cs b/src/Avalonia.Visuals/Media/UnicodeRange.cs new file mode 100644 index 0000000000..6179b57ece --- /dev/null +++ b/src/Avalonia.Visuals/Media/UnicodeRange.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Avalonia.Media +{ + /// + /// The descripes a set of Unicode characters. + /// + public readonly struct UnicodeRange + { + public static UnicodeRange Default = Parse("0-10FFFD"); + + private readonly UnicodeRangeSegment _single; + private readonly IReadOnlyList _segments = null; + + public UnicodeRange(int start, int end) + { + _single = new UnicodeRangeSegment(start, end); + } + + public UnicodeRange(UnicodeRangeSegment single) + { + _single = single; + } + + public UnicodeRange(IReadOnlyList segments) + { + if(segments is null || segments.Count == 0) + { + throw new ArgumentException(nameof(segments)); + } + + _single = segments[0]; + _segments = segments; + } + + internal UnicodeRangeSegment Single => _single; + + internal IReadOnlyList Segments => _segments; + + /// + /// Determines if given value is inside the range. + /// + /// The value to verify. + /// + /// true If given value is inside the range, false otherwise. + /// + 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; + } + + /// + /// Parses a . + /// + /// The string to parse. + /// The parsed . + /// + 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; + } + + /// + /// Get the start of the segment. + /// + public int Start { get; } + + /// + /// Get the end of the segment. + /// + public int End { get; } + + /// + /// Determines if given value is inside the range segment. + /// + /// The value to verify. + /// + /// true If given value is inside the range segment, false otherwise. + /// + public bool IsInRange(int value) + { + return value - Start <= End - Start; + } + + /// + /// Parses a . + /// + /// The string to parse. + /// The parsed . + /// + 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); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf b/src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf deleted file mode 100644 index 3560a3a0c8..0000000000 Binary files a/src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf and /dev/null differ diff --git a/src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf b/src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf deleted file mode 100644 index 1639ad7d40..0000000000 Binary files a/src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf and /dev/null differ diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj index b0101891b1..8b7babe5b1 100644 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj @@ -34,7 +34,6 @@ - true build\ diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs index 85ac57c746..7970f09a58 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs +++ b/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().ToConstant(new FontManager(new CustomFontManagerImpl())); - return builder; } } diff --git a/src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs b/src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs deleted file mode 100644 index 01837bbcd4..0000000000 --- a/src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs +++ /dev/null @@ -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 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); - } - } -} diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs index 9c16041205..c9c59a6e32 100644 --- a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs +++ b/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 codepoints) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs index 2b0ffa4ed6..6e5b8eb637 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -30,5 +30,35 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Throws(() => 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().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().ToConstant(options); + + FontManager.Current.TryMatchCharacter(1, FontStyle.Normal, FontWeight.Normal, FontFamily.Default, null, out var typeface); + + Assert.Equal("MyFont", typeface.FontFamily.Name); + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeSegmentTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeSegmentTests.cs new file mode 100644 index 0000000000..200358d4b9 --- /dev/null +++ b/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); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeTests.cs new file mode 100644 index 0000000000..5edbd613eb --- /dev/null +++ b/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)); + } + } +}