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