Browse Source

Merge branch 'master' into feature/macIme

pull/9526/head
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
391160d563
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      native/Avalonia.Native/src/OSX/AvnView.mm
  2. 24
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  3. 5
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  4. 2
      samples/ControlCatalog.NetCore/Program.cs
  5. 2
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  6. 75
      samples/IntegrationTestApp/ShowWindowTest.axaml
  7. 19
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  8. 130
      src/Avalonia.Base/Media/FontManager.cs
  9. 290
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  10. 4
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  11. 66
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  12. 33
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  13. 107
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  14. 20
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  15. 15
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  16. 13
      src/Avalonia.Base/Media/Typeface.cs
  17. 30
      src/Avalonia.Base/Platform/IFontManagerImpl.cs
  18. 5
      src/Avalonia.Base/Utilities/UriExtensions.cs
  19. 15
      src/Avalonia.Controls/AppBuilder.cs
  20. 13
      src/Avalonia.Fonts.Inter/AppBuilderExtension.cs
  21. 1
      src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj
  22. 14
      src/Avalonia.Fonts.Inter/InterFontCollection.cs
  23. 29
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  24. 2
      src/Avalonia.Themes.Fluent/Accents/Base.xaml
  25. 2
      src/Avalonia.Themes.Simple/Accents/Base.xaml
  26. 85
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  27. 14
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  28. 198
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  29. 73
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  30. 13
      src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs
  31. 13
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  32. 57
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  33. 20
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  34. 6
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  35. 16
      tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs
  36. 61
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  37. 19
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  38. 6
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  39. 32
      tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
  40. 116
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  41. 68
      tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs
  42. 76
      tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs
  43. 63
      tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
  44. 37
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  45. 9
      tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
  46. 20
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  47. 8
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs

7
native/Avalonia.Native/src/OSX/AvnView.mm

@ -131,11 +131,8 @@
[self updateRenderTarget];
auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
if(_parent->IsShown())
{
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
}
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
}
}

24
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -4,6 +4,7 @@
//
#import <AppKit/AppKit.h>
#import <Cocoa/Cocoa.h>
#include "common.h"
#include "AvnView.h"
#include "menu.h"
@ -295,15 +296,24 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso
}
@try {
if(x != lastSize.width || y != lastSize.height) {
lastSize = NSSize{x, y};
if(x != lastSize.width || y != lastSize.height)
{
if (!_shown) {
BaseEvents->Resized(AvnSize{x, y}, reason);
} else if (Window != nullptr) {
[Window setContentSize:lastSize];
[Window invalidateShadow];
auto screenSize = [Window screen].visibleFrame.size;
if (x > screenSize.width) {
x = screenSize.width;
}
if (y > screenSize.height) {
y = screenSize.height;
}
}
lastSize = NSSize{x, y};
[Window setContentSize:lastSize];
[Window invalidateShadow];
}
}
@finally {

5
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -54,6 +54,11 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
WindowBaseImpl::Show(activate, isDialog);
GetWindowState(&_actualWindowState);
if(IsZoomed()) {
_lastWindowState = _actualWindowState;
}
return SetWindowState(_lastWindowState);
}
}

2
samples/ControlCatalog.NetCore/Program.cs

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Fonts.Inter;
using Avalonia.Headless;
using Avalonia.LogicalTree;
using Avalonia.Threading;
@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore
EnableIme = true
})
.UseSkia()
.WithInterFont()
.AfterSetup(builder =>
{
builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()

2
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Get<ComboBox>("fontComboBox");
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.Items = FontManager.Current.SystemFonts;
fontComboBox.SelectedIndex = 0;
}
}

75
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -1,41 +1,48 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:integrationTestApp="clr-namespace:IntegrationTestApp"
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
x:DataType="Window"
Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
<TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
<TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True"/>
<Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
<integrationTestApp:MeasureBorder Name="MyBorder">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}" />
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
<TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
<TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True" />
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
<TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
<Label Grid.Row="9" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
</Grid>
</integrationTestApp:MeasureBorder>
</Window>

19
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -7,6 +7,25 @@ using Avalonia.Threading;
namespace IntegrationTestApp
{
public class MeasureBorder : Border
{
protected override Size MeasureOverride(Size availableSize)
{
MeasuredWith = availableSize;
return base.MeasureOverride(availableSize);
}
public static readonly StyledProperty<Size> MeasuredWithProperty = AvaloniaProperty.Register<MeasureBorder, Size>(
nameof(MeasuredWith));
public Size MeasuredWith
{
get => GetValue(MeasuredWithProperty);
set => SetValue(MeasuredWithProperty, value);
}
}
public class ShowWindowTest : Window
{
private readonly DispatcherTimer? _timer;

130
src/Avalonia.Base/Media/FontManager.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media
{
@ -13,9 +15,11 @@ namespace Avalonia.Media
/// </summary>
public sealed class FontManager
{
private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, IGlyphTypeface>();
private readonly FontFamily _defaultFontFamily;
internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
public const string FontCollectionScheme = "fonts";
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
public FontManager(IFontManagerImpl platformImpl)
@ -33,9 +37,12 @@ namespace Avalonia.Media
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
AddFontCollection(new SystemFontCollection(this));
}
/// <summary>
/// Get the current font manager instance.
/// </summary>
public static FontManager Current
{
get
@ -57,11 +64,6 @@ namespace Avalonia.Media
}
}
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
@ -71,41 +73,109 @@ namespace Avalonia.Media
}
/// <summary>
/// Get all installed font family names.
/// Get all system fonts.
/// </summary>
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public IFontCollection SystemFonts => _fontCollections[SystemFontsKey];
internal IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Returns a new <see cref="IGlyphTypeface"/>, or an existing one if a matching <see cref="IGlyphTypeface"/> exists.
/// Tries to get a glyph typeface for specified typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// The <see cref="IGlyphTypeface"/>.
/// <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface)
public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
while (true)
glyphTypeface = null;
var fontFamily = typeface.FontFamily;
if (fontFamily.Key is FontFamilyKey key)
{
if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface))
var source = key.Source;
if (!source.IsAbsoluteUri)
{
return glyphTypeface;
if (key.BaseUri == null)
{
throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
}
source = new Uri(key.BaseUri, source);
}
glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface);
if (!_fontCollections.TryGetValue(source, out var fontCollection))
{
var embeddedFonts = new EmbeddedFontCollection(source, source);
embeddedFonts.Initialize(PlatformImpl);
if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface))
if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
{
fontCollection = embeddedFonts;
}
}
if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return glyphTypeface;
return true;
}
if (typeface.FontFamily == _defaultFontFamily)
if (!fontFamily.FamilyNames.HasFallbacks)
{
throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
return false;
}
}
typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight);
foreach (var familyName in fontFamily.FamilyNames)
{
if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
}
}
return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface);
}
/// <summary>
/// Add a font collection to the manager.
/// </summary>
/// <param name="fontCollection">The font collection.</param>
/// <exception cref="ArgumentException"></exception>
/// <remarks>If a font collection's key is already present the collection is replaced.</remarks>
public void AddFontCollection(IFontCollection fontCollection)
{
var key = fontCollection.Key;
if (!fontCollection.Key.IsFontCollection())
{
throw new ArgumentException("Font collection Key should follow the fonts: scheme.", nameof(fontCollection));
}
_fontCollections.AddOrUpdate(key, fontCollection, (_, oldCollection) =>
{
oldCollection.Dispose();
return fontCollection;
});
fontCollection.Initialize(PlatformImpl);
}
/// <summary>
/// Removes the font collection that corresponds to specified key.
/// </summary>
/// <param name="key">The font collection's key.</param>
public void RemoveFontCollection(Uri key)
{
if (_fontCollections.TryRemove(key, out var fontCollection))
{
fontCollection.Dispose();
}
}
@ -123,18 +193,16 @@ namespace Avalonia.Media
/// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch,
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
{
if(_fontFallbacks != null)
if (_fontFallbacks != null)
{
foreach (var fallback in _fontFallbacks)
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
return true;
}
}

290
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -0,0 +1,290 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
public class EmbeddedFontCollection : IFontCollection
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
private readonly Uri _key;
private readonly Uri _source;
public EmbeddedFontCollection(Uri key, Uri source)
{
_key = key;
_source = source;
}
public Uri Key => _key;
public FontFamily this[int index] => _fontFamilies[index];
public int Count => _fontFamilies.Count;
public void Initialize(IFontManagerImpl fontManager)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
foreach (var fontAsset in fontAssets)
{
var stream = assetLoader.Open(fontAsset);
if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
{
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
{
_fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
}
}
var key = new FontCollectionKey(
glyphTypeface.Style,
glyphTypeface.Weight,
glyphTypeface.Stretch);
glyphTypefaces.TryAdd(key, glyphTypeface);
}
}
}
public void Dispose()
{
foreach (var fontFamily in _fontFamilies)
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces))
{
foreach (var glyphTypeface in glyphTypefaces.Values)
{
glyphTypeface.Dispose();
}
}
}
GC.SuppressFinalize(this);
}
public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
}
//Try to find a partially matching font
for (var i = 0; i < Count; i++)
{
var fontFamily = _fontFamilies[i];
if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
{
if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
}
}
glyphTypeface = null;
return false;
}
private static bool TryGetNearestMatch(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
if (key.Style != FontStyle.Normal)
{
key = key with { Style = FontStyle.Normal };
}
if (key.Stretch != FontStretch.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
{
return true;
}
}
key = key with { Stretch = FontStretch.Normal };
}
if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
{
return true;
}
//Take the first glyph typeface we can find.
foreach (var typeface in glyphTypefaces.Values)
{
glyphTypeface = typeface;
return true;
}
return false;
}
private static bool TryFindStretchFallback(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
glyphTypeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface))
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface))
{
return true;
}
}
}
return false;
}
private static bool TryFindWeightFallback(
ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
FontCollectionKey key,
[NotNullWhen(true)] out IGlyphTypeface? typeface)
{
typeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
{
return true;
}
}
}
return false;
}
}
}

4
src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs

@ -0,0 +1,4 @@
namespace Avalonia.Media.Fonts
{
public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch);
}

66
src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs

@ -11,22 +11,30 @@ namespace Avalonia.Media.Fonts
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
IsFontTtfOrOtf(fontFamilyKey.Source) ?
GetFontAssetsByExpression(fontFamilyKey) :
GetFontAssetsBySource(fontFamilyKey);
public static IEnumerable<Uri> LoadFontAssets(Uri source)
{
if (source.IsAvares() || source.IsAbsoluteResm())
{
return IsFontTtfOrOtf(source) ?
GetFontAssetsByExpression(source) :
GetFontAssetsBySource(source);
}
return Enumerable.Empty<Uri>();
}
/// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var availableAssets = assetLoader.GetAssets(source, null);
return availableAssets.Where(x => IsFontTtfOrOtf(x));
}
@ -34,60 +42,50 @@ namespace Avalonia.Media.Fonts
/// Searches for font assets at a given location and only accepts assets that fit to a given filename expression.
/// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
/// </summary>
/// <param name="fontFamilyKey"></param>
/// <param name="source"></param>
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
private static IEnumerable<Uri> GetFontAssetsByExpression(Uri source)
{
var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location);
var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension);
var (fileNameWithoutExtension, extension) = GetFileName(source, out var location);
var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
var availableResources = assetLoader.GetAssets(location, null);
return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
}
private static (string fileNameWithoutExtension, string extension) GetFileName(
FontFamilyKey fontFamilyKey, out Uri location)
Uri source, out Uri location)
{
if (fontFamilyKey.Source.IsAbsoluteResm())
if (source.IsAbsoluteResm())
{
var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.');
var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.');
var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri()
var uriLocation = source.GetUnescapeAbsoluteUri()
.Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
return fileName;
}
var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString);
var filename = GetFileNameAndExtension(source.OriginalString);
var fullFilename = filename.fileNameWithoutExtension + filename.extension;
if (fontFamilyKey.BaseUri != null)
{
var relativePath = fontFamilyKey.Source.OriginalString
.Replace(fullFilename, string.Empty);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
}
else
{
var uriString = fontFamilyKey.Source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
}
var uriString = source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
return filename;
}
private static string CreateFilePattern(
FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension)
Uri source, Uri location, string fileNameWithoutExtension)
{
var path = location.GetUnescapeAbsolutePath();
var file = GetSubString(fileNameWithoutExtension, '*');
return fontFamilyKey.Source.IsAbsoluteResm()
return source.IsAbsoluteResm()
? path + "." + file
: path + file;
}

33
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable
{
/// <summary>
/// Get the font collection's key.
/// </summary>
Uri Key { get; }
/// <summary>
/// Initializes the font collection.
/// </summary>
/// <param name="fontManager">The font manager the collection is registered with.</param>
void Initialize(IFontManagerImpl fontManager);
/// <summary>
/// Try to get a glyph typeface for given parameters.
/// </summary>
/// <param name="familyName">The family name.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
}
}

107
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -0,0 +1,107 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
namespace Avalonia.Media.Fonts
{
internal class SystemFontCollection : IFontCollection
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
private readonly FontManager _fontManager;
private readonly string[] _familyNames;
public SystemFontCollection(FontManager fontManager)
{
_fontManager = fontManager;
_familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames();
}
public Uri Key => FontManager.SystemFontsKey;
public FontFamily this[int index]
{
get
{
var familyName = _familyNames[index];
return new FontFamily(familyName);
}
}
public int Count => _familyNames.Length;
public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
if (familyName == FontFamily.DefaultFontFamilyName)
{
familyName = _fontManager.DefaultFontFamilyName;
}
var key = new FontCollectionKey(style, weight, stretch);
if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
{
if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
{
return true;
}
else
{
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) &&
glyphTypefaces.TryAdd(key, glyphTypeface))
{
return true;
}
}
}
if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
{
glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces))
{
return true;
}
}
return false;
}
public void Initialize(IFontManagerImpl fontManager)
{
//We initialize the system font collection during construction.
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<FontFamily> GetEnumerator()
{
foreach (var familyName in _familyNames)
{
yield return new FontFamily(familyName);
}
}
void IDisposable.Dispose()
{
foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
{
foreach (var pair in glyphTypefaces)
{
pair.Value.Dispose();
}
}
GC.SuppressFinalize(this);
}
}
}

20
src/Avalonia.Base/Media/IGlyphTypeface.cs

@ -6,6 +6,26 @@ namespace Avalonia.Media
[Unstable]
public interface IGlyphTypeface : IDisposable
{
/// <summary>
/// Gets the family name for the <see cref="IGlyphTypeface"/> object.
/// </summary>
string FamilyName { get; }
/// <summary>
/// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontWeight Weight { get; }
/// <summary>
/// Gets the style for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStyle Style { get; }
/// <summary>
/// Gets the <see cref="FontStretch"/> value for the <see cref="IGlyphTypeface"/> object.
/// </summary>
FontStretch Stretch { get; }
/// <summary>
/// Gets the number of glyphs held by this glyph typeface.
/// </summary>

15
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting
if (matchFound)
{
// Fallback found
var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface))
{
if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
}
}
// no fallback found

13
src/Avalonia.Base/Media/Typeface.cs

@ -80,7 +80,18 @@ namespace Avalonia.Media
/// <value>
/// The glyph typeface.
/// </value>
public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this);
public IGlyphTypeface GlyphTypeface
{
get
{
if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface))
{
return glyphTypeface;
}
throw new InvalidOperationException("Could not create glyphTypeface.");
}
}
public static bool operator !=(Typeface a, Typeface b)
{

30
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Metadata;
@ -17,7 +18,7 @@ namespace Avalonia.Platform
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
@ -37,12 +38,27 @@ namespace Avalonia.Platform
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
/// <summary>
/// Creates a glyph typeface.
/// Tries to get a glyph typeface for specified parameters.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>0
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
/// <param name="familyName">The family name.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weiht.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
/// <summary>
/// Tries to create a glyph typeface from specified stream.
/// </summary>
/// <param name="stream">A stream that holds the font's data.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns>
IGlyphTypeface CreateGlyphTypeface(Typeface typeface);
bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
}
}

5
src/Avalonia.Base/Utilities/UriExtensions.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Media;
namespace Avalonia.Utilities;
@ -10,7 +11,9 @@ internal static class UriExtensions
public static bool IsResm(this Uri uri) => uri.Scheme == "resm";
public static bool IsAvares(this Uri uri) => uri.Scheme == "avares";
public static bool IsFontCollection(this Uri uri) => uri.Scheme == FontManager.FontCollectionScheme;
public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri)
{
if (uri.IsAbsoluteUri)

15
src/Avalonia.Controls/AppBuilder.cs

@ -4,6 +4,8 @@ using System.Reflection;
using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Media.Fonts;
using Avalonia.Media;
namespace Avalonia
{
@ -205,6 +207,19 @@ namespace Avalonia
return Self;
}
/// <summary>
/// Registers an action that is executed with the current font manager.
/// </summary>
/// <param name="action">The action.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder ConfigureFonts(Action<FontManager> action)
{
return AfterSetup(appBuilder =>
{
action?.Invoke(FontManager.Current);
});
}
/// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary>

13
src/Avalonia.Fonts.Inter/AppBuilderExtension.cs

@ -0,0 +1,13 @@
namespace Avalonia.Fonts.Inter
{
public static class AppBuilderExtension
{
public static AppBuilder WithInterFont(this AppBuilder appBuilder)
{
return appBuilder.ConfigureFonts(fontManager =>
{
fontManager.AddFontCollection(new InterFontCollection());
});
}
}
}

1
src/Avalonia.Fonts.Inter/Avalonia.Fonts.Inter.csproj

@ -8,6 +8,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

14
src/Avalonia.Fonts.Inter/InterFontCollection.cs

@ -0,0 +1,14 @@
using System;
using Avalonia.Media.Fonts;
namespace Avalonia.Fonts.Inter
{
public sealed class InterFontCollection : EmbeddedFontCollection
{
public InterFontCollection() : base(
new Uri("fonts:Inter", UriKind.Absolute),
new Uri("avares://Avalonia.Fonts.Inter/Assets", UriKind.Absolute))
{
}
}
}

29
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -84,6 +84,14 @@ namespace Avalonia.Headless
public FontSimulations FontSimulations { get; }
public string FamilyName => "Arial";
public FontWeight Weight => FontWeight.Normal;
public FontStyle Style => FontStyle.Normal;
public FontStretch Stretch => FontStretch.Normal;
public void Dispose()
{
}
@ -147,19 +155,28 @@ namespace Avalonia.Headless
class HeadlessFontManagerStub : IFontManagerImpl
{
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public string GetDefaultFontFamilyName()
{
return new HeadlessGlyphTypefaceImpl();
return "Arial";
}
public string GetDefaultFontFamilyName()
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return "Arial";
return new string[] { "Arial" };
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
{
return new List<string> { "Arial" };
glyphTypeface= new HeadlessGlyphTypefaceImpl();
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new HeadlessGlyphTypefaceImpl();
return true;
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,

2
src/Avalonia.Themes.Fluent/Accents/Base.xaml

@ -3,7 +3,7 @@
xmlns:sys="using:System"
xmlns:converters="using:Avalonia.Controls.Converters">
<!-- https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn518235(v=win.10)?redirectedfrom=MSDN -->
<FontFamily x:Key="ContentControlThemeFontFamily">avares://Avalonia.Fonts.Inter/Assets#Inter, $Default</FontFamily>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
<sys:Double x:Key="ControlContentThemeFontSize">14</sys:Double>
<SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />

2
src/Avalonia.Themes.Simple/Accents/Base.xaml

@ -76,7 +76,7 @@
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<FontFamily x:Key="ContentControlThemeFontFamily">avares://Avalonia.Fonts.Inter/Assets#Inter, $Default</FontFamily>
<FontFamily x:Key="ContentControlThemeFontFamily">fonts://Inter#Inter, $Default</FontFamily>
<Color x:Key="ThemeAccentColor">#CC119EDA</Color>
<Color x:Key="ThemeAccentColor2">#99119EDA</Color>
<Color x:Key="ThemeAccentColor3">#66119EDA</Color>

85
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
@ -16,14 +17,14 @@ namespace Avalonia.Skia
return SKTypeface.Default.FamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
{
_skFontManager = SKFontManager.CreateDefault();
}
return _skFontManager.FontFamilies;
return _skFontManager.GetFontFamilies();
}
[ThreadStatic] private static string[]? t_languageTagBuffer;
@ -95,72 +96,58 @@ namespace Avalonia.Skia
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
SKTypeface? skTypeface = null;
glyphTypeface = null;
if(typeface.FontFamily.Key is not null)
{
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
skTypeface = fontCollection.Get(typeface);
var fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
(SKFontStyleSlant)style);
if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
}
var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
if (skTypeface is null)
{
var defaultName = SKTypeface.Default.FamilyName;
var fontStyle = new SKFontStyle((SKFontStyleWeight)typeface.Weight, (SKFontStyleWidth)typeface.Stretch,
(SKFontStyleSlant)typeface.Style);
foreach (var familyName in typeface.FontFamily.FamilyNames)
{
if(familyName == FontFamily.DefaultFontFamilyName)
{
continue;
}
skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))
{
continue;
}
break;
}
// MatchTypeface can return "null" if matched typeface wasn't found for the style
// Fallback to the default typeface and styles instead.
skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle)
?? SKTypeface.Default;
return false;
}
if (skTypeface == null)
//MatchFamily can return a font other than we requested so we have to verify we got the expected.
if (!skTypeface.FamilyName.ToLower(CultureInfo.InvariantCulture).Equals(familyName.ToLower(CultureInfo.InvariantCulture), StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
return false;
}
var fontSimulations = FontSimulations.None;
if((int)typeface.Weight >= 600 && !skTypeface.IsBold)
if ((int)weight >= 600 && !skTypeface.IsBold)
{
fontSimulations |= FontSimulations.Bold;
}
if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic)
if (style == FontStyle.Italic && !skTypeface.IsItalic)
{
fontSimulations |= FontSimulations.Oblique;
}
return new GlyphTypefaceImpl(skTypeface, fontSimulations);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, fontSimulations);
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var skTypeface = SKTypeface.FromStream(stream);
if (skTypeface != null)
{
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
return true;
}
glyphTypeface = null;
return false;
}
}
}

14
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -51,6 +51,12 @@ namespace Avalonia.Skia
GlyphCount = Typeface.GlyphCount;
FontSimulations = fontSimulations;
Weight = (FontWeight)Typeface.FontWeight;
Style = Typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)Typeface.FontStyle.Width;
}
public Face Face { get; }
@ -67,6 +73,14 @@ namespace Avalonia.Skia
public int GlyphCount { get; }
public string FamilyName => Typeface.FamilyName;
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{
metrics = default;

198
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -1,198 +0,0 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces = new();
public void AddTypeface(Typeface key, SKTypeface typeface)
{
_typefaces.TryAdd(key, typeface);
}
public SKTypeface? Get(Typeface typeface)
{
return GetNearestMatch(typeface);
}
private SKTypeface? GetNearestMatch(Typeface key)
{
if (_typefaces.Count == 0)
{
return null;
}
if (_typefaces.TryGetValue(key, out var typeface))
{
return typeface;
}
if(key.Style != FontStyle.Normal)
{
key = new Typeface(key.FontFamily, FontStyle.Normal, key.Weight, key.Stretch);
}
if(key.Stretch != FontStretch.Normal)
{
if(TryFindStretchFallback(key, out typeface))
{
return typeface;
}
if(key.Weight != FontWeight.Normal)
{
if (TryFindStretchFallback(new Typeface(key.FontFamily, key.Style, FontWeight.Normal, key.Stretch), out typeface))
{
return typeface;
}
}
key = new Typeface(key.FontFamily, key.Style, key.Weight, FontStretch.Normal);
}
if(TryFindWeightFallback(key, out typeface))
{
return typeface;
}
if (TryFindStretchFallback(key, out typeface))
{
return typeface;
}
//Nothing was found so we try some regular typeface.
if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface))
{
return typeface;
}
SKTypeface? skTypeface = null;
foreach(var pair in _typefaces)
{
skTypeface = pair.Value;
if (skTypeface.FamilyName.Contains(key.FontFamily.Name))
{
return skTypeface;
}
}
return skTypeface;
}
private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
{
typeface = null;
var stretch = (int)key.Stretch;
if (stretch < 5)
{
for (var i = 0; stretch + i < 9; i++)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch + i)), out typeface))
{
return true;
}
}
}
else
{
for (var i = 0; stretch - i > 1; i++)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, key.Weight, (FontStretch)(stretch - i)), out typeface))
{
return true;
}
}
}
return false;
}
private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface)
{
typeface = null;
var weight = (int)key.Weight;
//If the target weight given is between 400 and 500 inclusive
if (weight >= 400 && weight <= 500)
{
//Look for available weights between the target and 500, in ascending order.
for (var i = 0; weight + i <= 500; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights greater than 500, in ascending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
}
//If a weight less than 400 is given, look for available weights less than the target, in descending order.
if (weight < 400)
{
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
}
//If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
if (weight > 500)
{
for (var i = 0; weight + i <= 900; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight + i), key.Stretch), out typeface))
{
return true;
}
}
//If no match is found, look for available weights less than the target, in descending order.
for (var i = 0; weight - i >= 100; i += 50)
{
if (_typefaces.TryGetValue(new Typeface(key.FontFamily, key.Style, (FontWeight)(weight - i), key.Stretch), out typeface))
{
return true;
}
}
}
return false;
}
}
}

73
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -1,73 +0,0 @@
using System;
using System.Collections.Concurrent;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
internal static class SKTypefaceCollectionCache
{
private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections;
static SKTypefaceCollectionCache()
{
s_cachedCollections = new ConcurrentDictionary<FontFamily, SKTypefaceCollection>();
}
/// <summary>
/// Gets the or add typeface collection.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <returns></returns>
public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily)
{
return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection);
}
/// <summary>
/// Creates the custom font collection.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <returns></returns>
private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily)
{
var typeFaceCollection = new SKTypefaceCollection();
if (fontFamily.Key is not { } fontFamilyKey)
{
return typeFaceCollection;
}
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets)
{
var assetStream = assetLoader.Open(asset);
if (assetStream == null)
throw new InvalidOperationException("Asset could not be loaded.");
var typeface = SKTypeface.FromStream(assetStream);
if (typeface == null)
throw new InvalidOperationException("Typeface could not be loaded.");
if (!typeface.FamilyName.Contains(fontFamily.Name))
{
continue;
}
var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(),
(FontWeight)typeface.FontWeight, (FontStretch)typeface.FontWidth);
typeFaceCollection.AddTypeface(key, typeface);
}
return typeFaceCollection;
}
}
}

13
src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs

@ -1,11 +1,10 @@
using System.Collections.Generic;
using Avalonia.Platform;
using SharpDX;
using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media
{
using System;
using System.IO;
internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
{
@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media
/// </summary>
/// <param name="factory">The factory.</param>
/// <param name="fontAssets"></param>
public DWriteResourceFontLoader(Factory factory, IEnumerable<Uri> fontAssets)
public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets)
{
var factory1 = factory;
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets)
{
var assetStream = assetLoader.Open(asset);
var dataStream = new DataStream((int)assetStream.Length, true, true);
var dataStream = new DataStream((int)asset.Length, true, true);
assetStream.CopyTo(dataStream);
asset.CopyTo(dataStream);
dataStream.Position = 0;

13
src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs

@ -6,6 +6,9 @@ using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = SharpDX.DirectWrite.FontStyle;
using FontWeight = SharpDX.DirectWrite.FontWeight;
using FontStretch = SharpDX.DirectWrite.FontStretch;
using Avalonia.Platform;
using System.Linq;
using System;
namespace Avalonia.Direct2D1.Media
{
@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media
private static FontCollection CreateFontCollection(FontFamilyKey key)
{
var assets = FontFamilyLoader.LoadFontAssets(key);
var source = key.BaseUri != null ? new Uri(key.BaseUri, key.Source) : key.Source;
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
var assets = FontFamilyLoader.LoadFontAssets(source);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontAssets = assets.Select(x => assetLoader.Open(x)).ToArray();
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, fontAssets);
return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
}

57
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStretch = Avalonia.Media.FontStretch;
using FontStyle = Avalonia.Media.FontStyle;
@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media
return "Segoe UI";
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
return new GlyphTypefaceImpl(typeface);
var systemFonts = Direct2D1FontCollectionCache.InstalledFontCollection;
if (familyName == FontFamily.DefaultFontFamilyName)
{
familyName = "Segoe UI";
}
if (systemFonts.FindFamilyName(familyName, out var index))
{
var font = systemFonts.GetFontFamily(index).GetFirstMatchingFont(
(SharpDX.DirectWrite.FontWeight)weight,
(SharpDX.DirectWrite.FontStretch)stretch,
(SharpDX.DirectWrite.FontStyle)style);
glyphTypeface = new GlyphTypefaceImpl(font);
return true;
}
glyphTypeface = null;
return false;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, new[] { stream });
var fontCollection = new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
if (fontCollection.FontFamilyCount > 0)
{
var fontFamily = fontCollection.GetFontFamily(0);
if (fontFamily.FontCount > 0)
{
var font = fontFamily.GetFont(0);
glyphTypeface = new GlyphTypefaceImpl(font);
return true;
}
}
glyphTypeface = null;
return false;
}
}
}

20
src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs

@ -12,9 +12,9 @@ namespace Avalonia.Direct2D1.Media
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font)
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
DWFont = font;
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media
StrikethroughThickness = strikethroughThickness,
IsFixedPitch = FontFace.IsMonospacedFont
};
FamilyName = DWFont.FontFamily.FamilyNames.GetString(0);
Weight = (Avalonia.Media.FontWeight)DWFont.Weight;
Style = (Avalonia.Media.FontStyle)DWFont.Style;
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch;
}
private Blob GetTable(Face face, Tag tag)
@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media
public FontSimulations FontSimulations => FontSimulations.None;
public string FamilyName { get; }
public Avalonia.Media.FontWeight Weight { get; }
public Avalonia.Media.FontStyle Style { get; }
public Avalonia.Media.FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)
{

6
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@ -16,9 +16,11 @@ namespace Avalonia.Base.UnitTests.Media
var typeface = new Typeface(fontFamily);
var glyphTypeface = FontManager.Current.GetOrAddGlyphTypeface(typeface);
Assert.True(FontManager.Current.TryGetGlyphTypeface(typeface, out var glyphTypeface));
Assert.Same(glyphTypeface, FontManager.Current.GetOrAddGlyphTypeface(typeface));
FontManager.Current.TryGetGlyphTypeface(typeface, out var other);
Assert.Same(glyphTypeface, other);
}
}

16
tests/Avalonia.Base.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs

@ -46,9 +46,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset()
{
var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets);
}
@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
{
var source = new Uri(AssetYourFontAvares);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets);
}
@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{
var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
var baseUri = new Uri(AssetLocationAvares);
var key = new FontFamilyKey(source, baseUri);
var fontAssets = FontFamilyLoader.LoadFontAssets(key);
var fontAssets = FontFamilyLoader.LoadFontAssets(new Uri(baseUri, source));
Assert.Single(fontAssets);
}
@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Matching_Assets()
{
var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray();
var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
foreach (var fontAsset in fontAssets)
{
@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var fontFamily = new FontFamily("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono");
var source = new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono", UriKind.RelativeOrAbsolute);
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key).ToArray();
var fontAssets = FontFamilyLoader.LoadFontAssets(source).ToArray();
Assert.NotEmpty(fontAssets);

61
tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs

@ -1,4 +1,5 @@
using Avalonia.Direct2D1.Media;
using System;
using Avalonia.Direct2D1.Media;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial")));
var font = glyphTypeface.DWFont;
Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
var glyphTypeface =
new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal("Arial", glyphTypeface.FamilyName);
}
}
@ -38,18 +31,13 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold));
var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var font = glyphTypeface.DWFont;
Assert.Equal("Arial", glyphTypeface.FamilyName);
Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(FontWeight.Bold, glyphTypeface.Weight);
Assert.Equal(SharpDX.DirectWrite.FontWeight.Bold, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal(FontStyle.Normal, glyphTypeface.Style);
}
}
@ -60,20 +48,11 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var defaultName = FontManager.Current.DefaultFontFamilyName;
var font = glyphTypeface.DWFont;
var defaultName = fontManager.GetDefaultFontFamilyName();
Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
Assert.Equal(defaultName, glyphTypeface.FamilyName);
}
}
@ -86,12 +65,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri));
var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
var font = glyphTypeface.DWFont;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
@ -102,14 +78,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var font = glyphTypeface.DWFont;
var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
}

19
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
@ -143,6 +144,24 @@ namespace Avalonia.IntegrationTests.Appium
}
}
[Fact]
public void Showing_Window_With_Size_Larger_Than_Screen_Measures_Content_With_Working_Area()
{
using (OpenWindow(new Size(4000, 2200), ShowWindowMode.NonOwned, WindowStartupLocation.Manual))
{
var screenRectTextBox = _session.FindElementByAccessibilityId("CurrentClientSize");
var measuredWithTextBlock = _session.FindElementByAccessibilityId("CurrentMeasuredWithText");
var measuredWithString = measuredWithTextBlock.Text;
var workingAreaString = screenRectTextBox.Text;
var workingArea = Size.Parse(workingAreaString);
var measuredWith = Size.Parse(measuredWithString);
Assert.Equal(workingArea, measuredWith);
}
}
[Theory]
[InlineData(ShowWindowMode.NonOwned)]

6
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@ -14,6 +15,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class StyleTests : XamlTestBase
{
static StyleTests()
{
GC.KeepAlive(typeof(ItemsRepeater));
}
[Fact]
public void Color_Can_Be_Added_To_Style_Resources()
{

32
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@ -17,11 +17,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
public class TextLayoutTests : TestBase
{
private const string FontName = "Courier New";
private const double FontSize = 12;
private const double MediumFontSize = 18;
private const double BigFontSize = 32;
private const double FontSizeHeight = 13.594;//real value 13.59375
private const double FontSizeHeight = 14.0625;//real value 13.59375
private const string stringword = "word";
private const string stringmiddle = "The quick brown fox jumps over the lazy dog";
private const string stringmiddle2lines = "The quick brown fox\njumps over the lazy dog";
@ -40,7 +39,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media
}
private static TextLayout Create(string text,
string fontFamily,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment,
@ -48,7 +46,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
TextWrapping wrapping,
double widthConstraint)
{
var typeface = new Typeface(fontFamily, fontStyle, fontWeight);
var typeface = new Typeface(TestFontFamily, fontStyle, fontWeight);
var formattedText = new TextLayout(text, typeface, fontSize, null, textAlignment, wrapping,
maxWidth: widthConstraint == -1 ? double.PositiveInfinity : widthConstraint);
@ -58,7 +56,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
private static TextLayout Create(string text, double fontSize)
{
return Create(text, FontName, fontSize,
return Create(text, fontSize,
FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, TextWrapping.NoWrap,
-1);
@ -66,7 +64,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
private static TextLayout Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
{
return Create(text, FontName, fontSize,
return Create(text, fontSize,
FontStyle.Normal, alignment,
FontWeight.Normal, TextWrapping.NoWrap,
widthConstraint);
@ -74,7 +72,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
private static TextLayout Create(string text, double fontSize, TextWrapping wrap, double widthConstraint)
{
return Create(text, FontName, fontSize,
return Create(text, fontSize,
FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, wrap,
widthConstraint);
@ -86,11 +84,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media
[InlineData("x", FontSize, 7.20, FontSizeHeight)]
[InlineData(stringword, FontSize, 28.80, FontSizeHeight)]
[InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)]
[InlineData(stringmiddle, MediumFontSize, 464.48, 20.391)]
[InlineData(stringmiddle, BigFontSize, 825.73, 36.25)]
[InlineData(stringmiddle, MediumFontSize, 464.48, 21.09375)]
[InlineData(stringmiddle, BigFontSize, 825.73, 37.5)]
[InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)]
[InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 20.391)]
[InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 36.25)]
[InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 21.09375)]
[InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 37.5)]
[InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)]
[InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
@ -221,12 +219,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media
}
[Theory]
[InlineData("x", 0, 1, "0,0,7.20,13.59")]
[InlineData(stringword, 0, 4, "0,0,28.80,13.59")]
[InlineData(stringmiddlenewlines, 10, 10, "0,13.59,57.61,13.59")]
[InlineData(stringmiddlenewlines, 10, 20, "0,13.59,57.61,13.59;0,27.19,64.81,13.59")]
[InlineData(stringmiddlenewlines, 10, 15, "0,13.59,57.61,13.59;0,27.19,36.01,13.59")]
[InlineData(stringmiddlenewlines, 15, 15, "36.01,13.59,21.60,13.59;0,27.19,64.81,13.59")]
[InlineData("x", 0, 1, "0,0,7.20,14.0625")]
[InlineData(stringword, 0, 4, "0,0,28.80,14.0625")]
[InlineData(stringmiddlenewlines, 10, 10, "0,14.0625,57.61,14.0625")]
[InlineData(stringmiddlenewlines, 10, 20, "0,14.0625,57.61,14.0625;0,28.125,64.81,14.0625")]
[InlineData(stringmiddlenewlines, 10, 15, "0,14.0625,57.61,14.0625;0,28.125,36.01,14.0625")]
[InlineData(stringmiddlenewlines, 15, 15, "36.01,14.0625,21.60,14.0625;0,28.125,64.81,14.0625")]
public void Should_HitTestRange_Correctly(string input,
int index, int length,
string expectedRects)

116
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@ -1,10 +1,12 @@
using System.Collections.Generic;
using System;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Avalonia.Skia.UnitTests.Media
{
@ -35,9 +37,9 @@ namespace Avalonia.Skia.UnitTests.Media
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return _customTypefaces.Select(x => x.FontFamily.Name);
return _customTypefaces.Select(x => x.FontFamily.Name).ToArray();
}
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
@ -70,48 +72,132 @@ namespace Avalonia.Skia.UnitTests.Media
{
SKTypeface skTypeface;
Uri source = null;
switch (typeface.FontFamily.Name)
{
case "Twitter Color Emoji":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _emojiTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _italicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Arabic":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _arabicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Hebrew":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
source = _hebrewTypeface.FontFamily.Key.Source;
break;
}
case FontFamily.DefaultFontFamilyName:
case "Noto Mono":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
skTypeface = typefaceCollection.Get(_defaultTypeface);
source = _defaultTypeface.FontFamily.Key.Source;
break;
}
default:
{
skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
break;
}
}
if (source is null)
{
skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
}
else
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var assetUri = FontFamilyLoader.LoadFontAssets(source).First();
var stream = assetLoader.Open(assetUri);
skTypeface = SKTypeface.FromStream(stream);
}
return new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
}
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
SKTypeface skTypeface;
Uri source = null;
switch (familyName)
{
case "Twitter Color Emoji":
{
source = _emojiTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans":
{
source = _italicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Arabic":
{
source = _arabicTypeface.FontFamily.Key.Source;
break;
}
case "Noto Sans Hebrew":
{
source = _hebrewTypeface.FontFamily.Key.Source;
break;
}
case FontFamily.DefaultFontFamilyName:
case "Noto Mono":
{
source = _defaultTypeface.FontFamily.Key.Source;
break;
}
default:
{
break;
}
}
if (source is null)
{
skTypeface = SKTypeface.FromFamilyName(familyName,
(SKFontStyleWeight)weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)style);
}
else
{
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var assetUri = FontFamilyLoader.LoadFontAssets(source).First();
var stream = assetLoader.Open(assetUri);
skTypeface = SKTypeface.FromStream(stream);
}
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
var skTypeface = SKTypeface.FromStream(stream);
glyphTypeface = new GlyphTypefaceImpl(skTypeface, FontSimulations.None);
return true;
}
}
}

68
tests/Avalonia.Skia.UnitTests/Media/EmbeddedFontCollectionTests.cs

@ -0,0 +1,68 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media
{
public class EmbeddedFontCollectionTests
{
private const string s_notoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
[InlineData(FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(FontWeight.Bold, FontStyle.Italic)]
[InlineData(FontWeight.Heavy, FontStyle.Oblique)]
[Theory]
public void Should_Get_Near_Matching_Typeface(FontWeight fontWeight, FontStyle fontStyle)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var source = new Uri(s_notoMono, UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.True(fontCollection.TryGetGlyphTypeface("Noto Mono", fontStyle, fontWeight, FontStretch.Normal, out var glyphTypeface));
var actual = glyphTypeface?.FamilyName;
Assert.Equal("Noto Mono", actual);
}
}
[Fact]
public void Should_Not_Get_Typeface_For_Invalid_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var source = new Uri(s_notoMono, UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.False(fontCollection.TryGetGlyphTypeface("ABC", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
}
}
[Fact]
public void Should_Get_Typeface_For_Partial_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var source = new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T", UriKind.Absolute);
var fontCollection = new EmbeddedFontCollection(source, source);
fontCollection.Initialize(new CustomFontManagerImpl());
Assert.True(fontCollection.TryGetGlyphTypeface("T", FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, out var glyphTypeface));
Assert.Equal("Twitter Color Emoji", glyphTypeface.FamilyName);
}
}
}
}

76
tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs

@ -14,92 +14,66 @@ namespace Avalonia.Skia.UnitTests.Media
[Fact]
public void Should_Create_Typeface_From_Fallback()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = FontManager.Current;
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
var glyphTypeface = new Typeface(new FontFamily("A, B, " + fontManager.DefaultFontFamilyName)).GlyphTypeface;
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
Assert.Equal(SKTypeface.Default.FamilyName, glyphTypeface.FamilyName);
}
}
[Fact]
public void Should_Create_Typeface_From_Fallback_Bold()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold));
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var skTypeface = glyphTypeface.Typeface;
Assert.True(skTypeface.FontWeight >= 600);
Assert.True((int)glyphTypeface.Weight >= 600);
}
}
[Fact]
public void Should_Create_Typeface_For_Unknown_Font()
public void Should_Yield_Default_GlyphTypeface_For_Invalid_FamilyName()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
Assert.Equal(FontManager.Current.DefaultFontFamilyName, glyphTypeface.FamilyName);
}
}
[Fact]
public void Should_Load_Typeface_From_Resource()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri));
var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
[Fact]
public void Should_Load_Nearest_Matching_Font()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
}
}
[Fact]
public void Should_Throw_For_Invalid_Custom_Font()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var fontManager = new FontManagerImpl();
Assert.Throws<InvalidOperationException>(() =>
fontManager.CreateGlyphTypeface(
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
Assert.Throws<InvalidOperationException>(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface);
}
}
}

63
tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs

@ -1,63 +0,0 @@
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests.Media
{
public class SKTypefaceCollectionCacheTests
{
private const string s_notoMono =
"resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
[InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)]
[InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)]
[InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)]
[Theory]
public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var fontFamily = new FontFamily(familyName);
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName;
Assert.Equal("Noto Mono", actual);
}
}
[Fact]
public void Should_Get_Typeface_For_Invalid_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var notoMono =
new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono);
var typeface = notoMonoCollection.Get(new Typeface("ABC"));
Assert.NotNull(typeface);
}
}
[Fact]
public void Should_Get_Typeface_For_Partial_FamilyName()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var fontFamily = new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#T");
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
var typeface = fontCollection.Get(new Typeface(fontFamily));
Assert.NotNull(typeface);
Assert.Equal("Twitter Color Emoji", typeface.FamilyName);
}
}
}
}

37
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.UnitTests
@ -31,9 +30,9 @@ namespace Avalonia.UnitTests
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return _customTypefaces.Select(x => x.FontFamily!.Name);
return _customTypefaces.Select(x => x.FontFamily!.Name).ToArray();
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch,
@ -58,29 +57,19 @@ namespace Avalonia.UnitTests
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
var fontFamily = typeface.FontFamily;
glyphTypeface = new HarfBuzzGlyphTypefaceImpl(stream);
if (fontFamily.IsDefault)
{
fontFamily = _defaultTypeface.FontFamily;
}
if (fontFamily!.Key == null)
{
return null;
}
var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
return true;
}
var asset = fontAssets.First();
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
glyphTypeface = null;
var stream = assetLoader.Open(asset);
return new HarfBuzzGlyphTypefaceImpl(stream);
return false;
}
}
}

9
tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs

@ -57,6 +57,15 @@ namespace Avalonia.UnitTests
public FontSimulations FontSimulations { get; }
public string FamilyName => "$Default";
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint)
{

20
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
@ -19,12 +20,12 @@ namespace Avalonia.UnitTests
return _defaultFamilyName;
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{
return new[] { _defaultFamilyName };
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, FontFamily fontFamily,
CultureInfo culture, out Typeface fontKey)
{
@ -33,9 +34,18 @@ namespace Avalonia.UnitTests
return false;
}
public IGlyphTypeface CreateGlyphTypeface(Typeface typeface)
public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
{
return new MockGlyphTypeface();
glyphTypeface = new MockGlyphTypeface();
return true;
}
public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
{
glyphTypeface = new MockGlyphTypeface();
return true;
}
}
}

8
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@ -17,6 +17,14 @@ namespace Avalonia.UnitTests
public FontSimulations FontSimulations => throw new NotImplementedException();
public string FamilyName => "$Default";
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
public ushort GetGlyph(uint codepoint)
{
return (ushort)codepoint;

Loading…
Cancel
Save