committed by
Dan Walmsley
13 changed files with 327 additions and 85 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue