Browse Source

Introduce FontManagerOptions (#7089)

* Introduce FontManagerOptions

* Add missing comments
pull/7163/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
2633cf3ba4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      src/Avalonia.Visuals/Media/FontFallback.cs
  2. 32
      src/Avalonia.Visuals/Media/FontManager.cs
  3. 11
      src/Avalonia.Visuals/Media/FontManagerOptions.cs
  4. 199
      src/Avalonia.Visuals/Media/UnicodeRange.cs
  5. BIN
      src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf
  6. BIN
      src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf
  7. 1
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  8. 2
      src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs
  9. 78
      src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs
  10. 2
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs
  11. 30
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  12. 22
      tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeSegmentTests.cs
  13. 17
      tests/Avalonia.Visuals.UnitTests/Media/UnicodeRangeTests.cs

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

32
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
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
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

Binary file not shown.

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

Binary file not shown.

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>

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

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

2
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
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
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
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));
}
}
}
Loading…
Cancel
Save