Browse Source

Merge branch 'master' into feature/macIme

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

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

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

2
samples/ControlCatalog.NetCore/Program.cs

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

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

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

75
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -1,41 +1,48 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:integrationTestApp="clr-namespace:IntegrationTestApp"
x:Class="IntegrationTestApp.ShowWindowTest" x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow" Name="SecondaryWindow"
x:DataType="Window" x:DataType="Window"
Title="Show Window Test"> Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> <integrationTestApp:MeasureBorder Name="MyBorder">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label> <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True" <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
Text="{Binding ClientSize, Mode=OneWay}"/> <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" <Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
Text="{Binding FrameSize, Mode=OneWay}"/> <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="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="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="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="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}"> <Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem> <ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem> <ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem> <ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem> <ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
</ComboBox> <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.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> <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> </Window>

19
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -7,6 +7,25 @@ using Avalonia.Threading;
namespace IntegrationTestApp 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 public class ShowWindowTest : Window
{ {
private readonly DispatcherTimer? _timer; private readonly DispatcherTimer? _timer;

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

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -13,9 +15,11 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public sealed class FontManager public sealed class FontManager
{ {
private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache = internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
new ConcurrentDictionary<Typeface, IGlyphTypeface>();
private readonly FontFamily _defaultFontFamily; public const string FontCollectionScheme = "fonts";
private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks; private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
public FontManager(IFontManagerImpl platformImpl) public FontManager(IFontManagerImpl platformImpl)
@ -33,9 +37,12 @@ namespace Avalonia.Media
throw new InvalidOperationException("Default font family name can't be null or empty."); 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 public static FontManager Current
{ {
get get
@ -57,11 +64,6 @@ namespace Avalonia.Media
} }
} }
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary> /// <summary>
/// Gets the system's default font family's name. /// Gets the system's default font family's name.
/// </summary> /// </summary>
@ -71,41 +73,109 @@ namespace Avalonia.Media
} }
/// <summary> /// <summary>
/// Get all installed font family names. /// Get all system fonts.
/// </summary> /// </summary>
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param> public IFontCollection SystemFonts => _fontCollections[SystemFontsKey];
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); internal IFontManagerImpl PlatformImpl { get; }
/// <summary> /// <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> /// </summary>
/// <param name="typeface">The typeface.</param> /// <param name="typeface">The typeface.</param>
/// <param name="glyphTypeface">The created glyphTypeface</param>
/// <returns> /// <returns>
/// The <see cref="IGlyphTypeface"/>. /// <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
/// </returns> /// </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. /// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns> /// </returns>
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
FontStretch fontStretch, FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
{ {
if(_fontFallbacks != null) if (_fontFallbacks != null)
{ {
foreach (var fallback in _fontFallbacks) foreach (var fallback in _fontFallbacks)
{ {
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = GetOrAddGlyphTypeface(typeface); if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
{
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
return true; 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> /// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/> /// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary> /// </summary>
/// <param name="fontFamilyKey"></param> /// <param name="source"></param>
/// <returns></returns> /// <returns></returns>
public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) => public static IEnumerable<Uri> LoadFontAssets(Uri source)
IsFontTtfOrOtf(fontFamilyKey.Source) ? {
GetFontAssetsByExpression(fontFamilyKey) : if (source.IsAvares() || source.IsAbsoluteResm())
GetFontAssetsBySource(fontFamilyKey); {
return IsFontTtfOrOtf(source) ?
GetFontAssetsByExpression(source) :
GetFontAssetsBySource(source);
}
return Enumerable.Empty<Uri>();
}
/// <summary> /// <summary>
/// Searches for font assets at a given location and returns a quantity of found assets /// Searches for font assets at a given location and returns a quantity of found assets
/// </summary> /// </summary>
/// <param name="fontFamilyKey"></param> /// <param name="source"></param>
/// <returns></returns> /// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey) private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
{ {
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); 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)); 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. /// 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> /// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
/// </summary> /// </summary>
/// <param name="fontFamilyKey"></param> /// <param name="source"></param>
/// <returns></returns> /// <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 (fileNameWithoutExtension, extension) = GetFileName(source, out var location);
var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension); var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension);
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); 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)); return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
} }
private static (string fileNameWithoutExtension, string extension) GetFileName( 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); .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
location = new Uri(uriLocation, UriKind.RelativeOrAbsolute); location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
return fileName; return fileName;
} }
var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString); var filename = GetFileNameAndExtension(source.OriginalString);
var fullFilename = filename.fileNameWithoutExtension + filename.extension; var fullFilename = filename.fileNameWithoutExtension + filename.extension;
if (fontFamilyKey.BaseUri != null) var uriString = source
{ .GetUnescapeAbsoluteUri()
var relativePath = fontFamilyKey.Source.OriginalString .Replace(fullFilename, string.Empty);
.Replace(fullFilename, string.Empty); location = new Uri(uriString);
location = new Uri(fontFamilyKey.BaseUri, relativePath);
}
else
{
var uriString = fontFamilyKey.Source
.GetUnescapeAbsoluteUri()
.Replace(fullFilename, string.Empty);
location = new Uri(uriString);
}
return filename; return filename;
} }
private static string CreateFilePattern( private static string CreateFilePattern(
FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension) Uri source, Uri location, string fileNameWithoutExtension)
{ {
var path = location.GetUnescapeAbsolutePath(); var path = location.GetUnescapeAbsolutePath();
var file = GetSubString(fileNameWithoutExtension, '*'); var file = GetSubString(fileNameWithoutExtension, '*');
return fontFamilyKey.Source.IsAbsoluteResm() return source.IsAbsoluteResm()
? path + "." + file ? path + "." + file
: 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] [Unstable]
public interface IGlyphTypeface : IDisposable 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> /// <summary>
/// Gets the number of glyphs held by this glyph typeface. /// Gets the number of glyphs held by this glyph typeface.
/// </summary> /// </summary>

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

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

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

@ -80,7 +80,18 @@ namespace Avalonia.Media
/// <value> /// <value>
/// The glyph typeface. /// The glyph typeface.
/// </value> /// </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) 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.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -17,7 +18,7 @@ namespace Avalonia.Platform
/// Get all installed fonts in the system. /// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param> /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary> /// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false); string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary> /// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties. /// 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); FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
/// <summary> /// <summary>
/// Creates a glyph typeface. /// Tries to get a glyph typeface for specified parameters.
/// </summary> /// </summary>
/// <param name="typeface">The typeface.</param> /// <param name="familyName">The family name.</param>
/// <returns>0 /// <param name="style">The font style.</param>
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface. /// <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> /// </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 System;
using Avalonia.Media;
namespace Avalonia.Utilities; namespace Avalonia.Utilities;
@ -10,7 +11,9 @@ internal static class UriExtensions
public static bool IsResm(this Uri uri) => uri.Scheme == "resm"; public static bool IsResm(this Uri uri) => uri.Scheme == "resm";
public static bool IsAvares(this Uri uri) => uri.Scheme == "avares"; 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) public static Uri EnsureAbsolute(this Uri uri, Uri? baseUri)
{ {
if (uri.IsAbsoluteUri) if (uri.IsAbsoluteUri)

15
src/Avalonia.Controls/AppBuilder.cs

@ -4,6 +4,8 @@ using System.Reflection;
using System.Linq; using System.Linq;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Media.Fonts;
using Avalonia.Media;
namespace Avalonia namespace Avalonia
{ {
@ -205,6 +207,19 @@ namespace Avalonia
return Self; 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> /// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>. /// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary> /// </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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
</Project> </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 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() public void Dispose()
{ {
} }
@ -147,19 +155,28 @@ namespace Avalonia.Headless
class HeadlessFontManagerStub : IFontManagerImpl 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, 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:sys="using:System"
xmlns:converters="using:Avalonia.Controls.Converters"> xmlns:converters="using:Avalonia.Controls.Converters">
<!-- https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn518235(v=win.10)?redirectedfrom=MSDN --> <!-- 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> <sys:Double x:Key="ControlContentThemeFontSize">14</sys:Double>
<SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" /> <SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />

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

@ -76,7 +76,7 @@
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" /> <SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent" />
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </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="ThemeAccentColor">#CC119EDA</Color>
<Color x:Key="ThemeAccentColor2">#99119EDA</Color> <Color x:Key="ThemeAccentColor2">#99119EDA</Color>
<Color x:Key="ThemeAccentColor3">#66119EDA</Color> <Color x:Key="ThemeAccentColor3">#66119EDA</Color>

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

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
@ -16,14 +17,14 @@ namespace Avalonia.Skia
return SKTypeface.Default.FamilyName; return SKTypeface.Default.FamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{ {
if (checkForUpdates) if (checkForUpdates)
{ {
_skFontManager = SKFontManager.CreateDefault(); _skFontManager = SKFontManager.CreateDefault();
} }
return _skFontManager.FontFamilies; return _skFontManager.GetFontFamilies();
} }
[ThreadStatic] private static string[]? t_languageTagBuffer; [ThreadStatic] private static string[]? t_languageTagBuffer;
@ -95,72 +96,58 @@ namespace Avalonia.Skia
return false; 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 fontStyle = new SKFontStyle((SKFontStyleWeight)weight, (SKFontStyleWidth)stretch,
{ (SKFontStyleSlant)style);
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
skTypeface = fontCollection.Get(typeface);
if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks) var skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
}
if (skTypeface is null) if (skTypeface is null)
{ {
var defaultName = SKTypeface.Default.FamilyName; return false;
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;
} }
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( return false;
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
} }
var fontSimulations = FontSimulations.None; var fontSimulations = FontSimulations.None;
if((int)typeface.Weight >= 600 && !skTypeface.IsBold) if ((int)weight >= 600 && !skTypeface.IsBold)
{ {
fontSimulations |= FontSimulations.Bold; fontSimulations |= FontSimulations.Bold;
} }
if(typeface.Style == FontStyle.Italic && !skTypeface.IsItalic) if (style == FontStyle.Italic && !skTypeface.IsItalic)
{ {
fontSimulations |= FontSimulations.Oblique; 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; GlyphCount = Typeface.GlyphCount;
FontSimulations = fontSimulations; FontSimulations = fontSimulations;
Weight = (FontWeight)Typeface.FontWeight;
Style = Typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)Typeface.FontStyle.Width;
} }
public Face Face { get; } public Face Face { get; }
@ -67,6 +73,14 @@ namespace Avalonia.Skia
public int GlyphCount { get; } 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) public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
{ {
metrics = default; 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 System.Collections.Generic;
using Avalonia.Platform;
using SharpDX; using SharpDX;
using SharpDX.DirectWrite; using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
using System; using System.IO;
internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
{ {
@ -18,19 +17,15 @@ namespace Avalonia.Direct2D1.Media
/// </summary> /// </summary>
/// <param name="factory">The factory.</param> /// <param name="factory">The factory.</param>
/// <param name="fontAssets"></param> /// <param name="fontAssets"></param>
public DWriteResourceFontLoader(Factory factory, IEnumerable<Uri> fontAssets) public DWriteResourceFontLoader(Factory factory, Stream[] fontAssets)
{ {
var factory1 = factory; var factory1 = factory;
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
foreach (var asset in fontAssets) foreach (var asset in fontAssets)
{ {
var assetStream = assetLoader.Open(asset); var dataStream = new DataStream((int)asset.Length, true, true);
var dataStream = new DataStream((int)assetStream.Length, true, true);
assetStream.CopyTo(dataStream); asset.CopyTo(dataStream);
dataStream.Position = 0; 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 FontStyle = SharpDX.DirectWrite.FontStyle;
using FontWeight = SharpDX.DirectWrite.FontWeight; using FontWeight = SharpDX.DirectWrite.FontWeight;
using FontStretch = SharpDX.DirectWrite.FontStretch; using FontStretch = SharpDX.DirectWrite.FontStretch;
using Avalonia.Platform;
using System.Linq;
using System;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
@ -53,9 +56,15 @@ namespace Avalonia.Direct2D1.Media
private static FontCollection CreateFontCollection(FontFamilyKey key) 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); 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.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily; using FontFamily = Avalonia.Media.FontFamily;
using FontStretch = Avalonia.Media.FontStretch; using FontStretch = Avalonia.Media.FontStretch;
using FontStyle = Avalonia.Media.FontStyle; using FontStyle = Avalonia.Media.FontStyle;
@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.Media
return "Segoe UI"; return "Segoe UI";
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
{ {
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -62,9 +62,56 @@ namespace Avalonia.Direct2D1.Media
return false; 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; 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>(); FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
@ -48,6 +48,14 @@ namespace Avalonia.Direct2D1.Media
StrikethroughThickness = strikethroughThickness, StrikethroughThickness = strikethroughThickness,
IsFixedPitch = FontFace.IsMonospacedFont 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) private Blob GetTable(Face face, Tag tag)
@ -83,6 +91,14 @@ namespace Avalonia.Direct2D1.Media
public FontSimulations FontSimulations => FontSimulations.None; 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"/> /// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint) 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 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() public void Should_Load_Single_FontAsset()
{ {
var source = new Uri(AssetMyFontRegular, UriKind.RelativeOrAbsolute); 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); Assert.Single(fontAssets);
} }
@ -57,9 +56,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Single_FontAsset_Avares_Without_BaseUri() public void Should_Load_Single_FontAsset_Avares_Without_BaseUri()
{ {
var source = new Uri(AssetYourFontAvares); var source = new Uri(AssetYourFontAvares);
var key = new FontFamilyKey(source);
var fontAssets = FontFamilyLoader.LoadFontAssets(key); var fontAssets = FontFamilyLoader.LoadFontAssets(source);
Assert.Single(fontAssets); Assert.Single(fontAssets);
} }
@ -69,9 +67,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{ {
var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute); var source = new Uri(AssetYourFileName, UriKind.RelativeOrAbsolute);
var baseUri = new Uri(AssetLocationAvares); 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); Assert.Single(fontAssets);
} }
@ -80,9 +77,8 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
public void Should_Load_Matching_Assets() public void Should_Load_Matching_Assets()
{ {
var source = new Uri(AssetLocation + ".MyFont*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute); 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) foreach (var fontAsset in fontAssets)
{ {
@ -99,9 +95,9 @@ namespace Avalonia.Base.UnitTests.Media.Fonts
{ {
var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>(); 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); 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.Media;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -16,18 +17,10 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var glyphTypeface =
new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface;
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);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style); Assert.Equal("Arial", glyphTypeface.FamilyName);
} }
} }
@ -38,18 +31,13 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold));
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(FontStyle.Normal, glyphTypeface.Style);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
} }
} }
@ -60,20 +48,11 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var defaultName = FontManager.Current.DefaultFontFamilyName;
new Typeface(new FontFamily("Unknown")));
var font = glyphTypeface.DWFont; Assert.Equal(defaultName, glyphTypeface.FamilyName);
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);
} }
} }
@ -86,12 +65,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
var fontManager = new FontManagerImpl(); var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var glyphTypeface = new Typeface(s_fontUri).GlyphTypeface;
new Typeface(s_fontUri));
var font = glyphTypeface.DWFont; Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
} }
} }
@ -102,14 +78,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
{ {
Direct2D1Platform.Initialize(); Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl(); var glyphTypeface = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var font = glyphTypeface.DWFont;
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.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Utilities;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using OpenQA.Selenium; using OpenQA.Selenium;
using OpenQA.Selenium.Appium; 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] [Theory]
[InlineData(ShowWindowMode.NonOwned)] [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.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
@ -14,6 +15,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{ {
public class StyleTests : XamlTestBase public class StyleTests : XamlTestBase
{ {
static StyleTests()
{
GC.KeepAlive(typeof(ItemsRepeater));
}
[Fact] [Fact]
public void Color_Can_Be_Added_To_Style_Resources() 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 public class TextLayoutTests : TestBase
{ {
private const string FontName = "Courier New";
private const double FontSize = 12; private const double FontSize = 12;
private const double MediumFontSize = 18; private const double MediumFontSize = 18;
private const double BigFontSize = 32; 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 stringword = "word";
private const string stringmiddle = "The quick brown fox jumps over the lazy dog"; 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"; 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, private static TextLayout Create(string text,
string fontFamily,
double fontSize, double fontSize,
FontStyle fontStyle, FontStyle fontStyle,
TextAlignment textAlignment, TextAlignment textAlignment,
@ -48,7 +46,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
TextWrapping wrapping, TextWrapping wrapping,
double widthConstraint) 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, var formattedText = new TextLayout(text, typeface, fontSize, null, textAlignment, wrapping,
maxWidth: widthConstraint == -1 ? double.PositiveInfinity : widthConstraint); maxWidth: widthConstraint == -1 ? double.PositiveInfinity : widthConstraint);
@ -58,7 +56,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
private static TextLayout Create(string text, double fontSize) private static TextLayout Create(string text, double fontSize)
{ {
return Create(text, FontName, fontSize, return Create(text, fontSize,
FontStyle.Normal, TextAlignment.Left, FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, TextWrapping.NoWrap, FontWeight.Normal, TextWrapping.NoWrap,
-1); -1);
@ -66,7 +64,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
private static TextLayout Create(string text, double fontSize, TextAlignment alignment, double widthConstraint) private static TextLayout Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
{ {
return Create(text, FontName, fontSize, return Create(text, fontSize,
FontStyle.Normal, alignment, FontStyle.Normal, alignment,
FontWeight.Normal, TextWrapping.NoWrap, FontWeight.Normal, TextWrapping.NoWrap,
widthConstraint); widthConstraint);
@ -74,7 +72,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
private static TextLayout Create(string text, double fontSize, TextWrapping wrap, double widthConstraint) 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, FontStyle.Normal, TextAlignment.Left,
FontWeight.Normal, wrap, FontWeight.Normal, wrap,
widthConstraint); widthConstraint);
@ -86,11 +84,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media
[InlineData("x", FontSize, 7.20, FontSizeHeight)] [InlineData("x", FontSize, 7.20, FontSizeHeight)]
[InlineData(stringword, FontSize, 28.80, FontSizeHeight)] [InlineData(stringword, FontSize, 28.80, FontSizeHeight)]
[InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)] [InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)]
[InlineData(stringmiddle, MediumFontSize, 464.48, 20.391)] [InlineData(stringmiddle, MediumFontSize, 464.48, 21.09375)]
[InlineData(stringmiddle, BigFontSize, 825.73, 36.25)] [InlineData(stringmiddle, BigFontSize, 825.73, 37.5)]
[InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)] [InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)]
[InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 20.391)] [InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 21.09375)]
[InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 36.25)] [InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 37.5)]
[InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)] [InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)]
[InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)] [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight) public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
@ -221,12 +219,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media
} }
[Theory] [Theory]
[InlineData("x", 0, 1, "0,0,7.20,13.59")] [InlineData("x", 0, 1, "0,0,7.20,14.0625")]
[InlineData(stringword, 0, 4, "0,0,28.80,13.59")] [InlineData(stringword, 0, 4, "0,0,28.80,14.0625")]
[InlineData(stringmiddlenewlines, 10, 10, "0,13.59,57.61,13.59")] [InlineData(stringmiddlenewlines, 10, 10, "0,14.0625,57.61,14.0625")]
[InlineData(stringmiddlenewlines, 10, 20, "0,13.59,57.61,13.59;0,27.19,64.81,13.59")] [InlineData(stringmiddlenewlines, 10, 20, "0,14.0625,57.61,14.0625;0,28.125,64.81,14.0625")]
[InlineData(stringmiddlenewlines, 10, 15, "0,13.59,57.61,13.59;0,27.19,36.01,13.59")] [InlineData(stringmiddlenewlines, 10, 15, "0,14.0625,57.61,14.0625;0,28.125,36.01,14.0625")]
[InlineData(stringmiddlenewlines, 15, 15, "36.01,13.59,21.60,13.59;0,27.19,64.81,13.59")] [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, public void Should_HitTestRange_Correctly(string input,
int index, int length, int index, int length,
string expectedRects) 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.Globalization;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
using System.Diagnostics.CodeAnalysis;
using System.IO;
namespace Avalonia.Skia.UnitTests.Media namespace Avalonia.Skia.UnitTests.Media
{ {
@ -35,9 +37,9 @@ namespace Avalonia.Skia.UnitTests.Media
return _defaultFamilyName; 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 }; private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
@ -70,48 +72,132 @@ namespace Avalonia.Skia.UnitTests.Media
{ {
SKTypeface skTypeface; SKTypeface skTypeface;
Uri source = null;
switch (typeface.FontFamily.Name) switch (typeface.FontFamily.Name)
{ {
case "Twitter Color Emoji": case "Twitter Color Emoji":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily); source = _emojiTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans": case "Noto Sans":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); source = _italicTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans Arabic": case "Noto Sans Arabic":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_arabicTypeface.FontFamily); source = _arabicTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans Hebrew": case "Noto Sans Hebrew":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily); source = _hebrewTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case FontFamily.DefaultFontFamilyName: case FontFamily.DefaultFontFamilyName:
case "Noto Mono": case "Noto Mono":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); source = _defaultTypeface.FontFamily.Key.Source;
skTypeface = typefaceCollection.Get(_defaultTypeface);
break; break;
} }
default: default:
{ {
skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
break; 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); 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] [Fact]
public void Should_Create_Typeface_From_Fallback() public void Should_Create_Typeface_From_Fallback()
{ {
var fontManager = new FontManagerImpl(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var fontManager = FontManager.Current;
new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
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] [Fact]
public void Should_Create_Typeface_From_Fallback_Bold() public void Should_Create_Typeface_From_Fallback_Bold()
{ {
var fontManager = new FontManagerImpl(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var glyphTypeface = new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface;
new Typeface(new FontFamily($"A, B, Arial"), weight: FontWeight.Bold));
var skTypeface = glyphTypeface.Typeface; Assert.True((int)glyphTypeface.Weight >= 600);
}
Assert.True(skTypeface.FontWeight >= 600);
} }
[Fact] [Fact]
public void Should_Create_Typeface_For_Unknown_Font() public void Should_Yield_Default_GlyphTypeface_For_Invalid_FamilyName()
{ {
var fontManager = new FontManagerImpl(); using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
{
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( var glyphTypeface = new Typeface(new FontFamily("Unknown")).GlyphTypeface;
new Typeface(new FontFamily("Unknown")));
var skTypeface = glyphTypeface.Typeface; Assert.Equal(FontManager.Current.DefaultFontFamilyName, glyphTypeface.FamilyName);
}
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
} }
[Fact] [Fact]
public void Should_Load_Typeface_From_Resource() 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 = new Typeface(s_fontUri).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri));
var skTypeface = glyphTypeface.Typeface; Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
Assert.Equal("Noto Mono", skTypeface.FamilyName);
} }
} }
[Fact] [Fact]
public void Should_Load_Nearest_Matching_Font() 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 = new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black).GlyphTypeface;
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black));
var skTypeface = glyphTypeface.Typeface; Assert.Equal("Noto Mono", glyphTypeface.FamilyName);
Assert.Equal("Noto Mono", skTypeface.FamilyName);
} }
} }
[Fact] [Fact]
public void Should_Throw_For_Invalid_Custom_Font() 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>(() => new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown").GlyphTypeface);
Assert.Throws<InvalidOperationException>(() =>
fontManager.CreateGlyphTypeface(
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
} }
} }
} }

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

9
tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs

@ -57,6 +57,15 @@ namespace Avalonia.UnitTests
public FontSimulations FontSimulations { get; } public FontSimulations FontSimulations { get; }
public string FamilyName => "$Default";
public FontWeight Weight { get; }
public FontStyle Style { get; }
public FontStretch Stretch { get; }
/// <inheritdoc cref="IGlyphTypeface"/> /// <inheritdoc cref="IGlyphTypeface"/>
public ushort GetGlyph(uint codepoint) 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.Globalization;
using System.IO;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@ -19,12 +20,12 @@ namespace Avalonia.UnitTests
return _defaultFamilyName; return _defaultFamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
{ {
return new[] { _defaultFamilyName }; 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, FontStretch fontStretch, FontFamily fontFamily,
CultureInfo culture, out Typeface fontKey) CultureInfo culture, out Typeface fontKey)
{ {
@ -33,9 +34,18 @@ namespace Avalonia.UnitTests
return false; 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 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) public ushort GetGlyph(uint codepoint)
{ {
return (ushort)codepoint; return (ushort)codepoint;

Loading…
Cancel
Save