Browse Source

Initial implementation of a font manager

pull/3195/head
Benedikt Schroeder 7 years ago
parent
commit
83bfb8ec70
  1. 3
      Avalonia.sln
  2. 6
      build/HarfBuzzSharp.props
  3. 4
      build/SkiaSharp.props
  4. 3
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  5. 5
      src/Avalonia.Controls/TextBlock.cs
  6. 23
      src/Avalonia.Visuals/Media/FontFamily.cs
  7. 100
      src/Avalonia.Visuals/Media/FontManager.cs
  8. 6
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  9. 47
      src/Avalonia.Visuals/Media/FormattedText.cs
  10. 108
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  11. 99
      src/Avalonia.Visuals/Media/Typeface.cs
  12. 57
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  13. 89
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  14. 7
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  15. 6
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  16. 1
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  17. 40
      src/Skia/Avalonia.Skia/FontKey.cs
  18. 91
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  19. 48
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  20. 179
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  21. 5
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  22. 91
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  23. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  24. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  25. 86
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  26. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  27. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  28. 17
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  29. 49
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  30. 80
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  31. 19
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  32. 188
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  33. 1
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  34. 3
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  35. 3
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  36. 1
      tests/Avalonia.UnitTests/TestServices.cs
  37. 44
      tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs
  38. 14
      tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
  39. 1
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

3
Avalonia.sln

@ -128,6 +128,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Base.props = build\Base.props build\Base.props = build\Base.props
build\Binding.props = build\Binding.props build\Binding.props = build\Binding.props
build\BuildTargets.targets = build\BuildTargets.targets build\BuildTargets.targets = build\BuildTargets.targets
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props
@ -201,7 +202,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
EndProject EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution

6
build/HarfBuzzSharp.props

@ -0,0 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.6.1-rc.153" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1-rc.153" />
</ItemGroup>
</Project>

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.0" /> <PackageReference Include="SkiaSharp" Version="1.68.1-rc.153" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" /> <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1-rc.153" />
</ItemGroup> </ItemGroup>
</Project> </Project>

3
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -297,7 +297,8 @@ namespace Avalonia.Controls.Presenters
return new FormattedText return new FormattedText
{ {
Text = "X", Text = "X",
Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
Constraint = availableSize, Constraint = availableSize,
}.Bounds.Size; }.Bounds.Size;

5
src/Avalonia.Controls/TextBlock.cs

@ -352,10 +352,11 @@ namespace Avalonia.Controls
return new FormattedText return new FormattedText
{ {
Constraint = constraint, Constraint = constraint,
Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Text = text ?? string.Empty, Text = text ?? string.Empty,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
Wrapping = TextWrapping, TextWrapping = TextWrapping,
}; };
} }

23
src/Avalonia.Visuals/Media/FontFamily.cs

@ -5,12 +5,16 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public class FontFamily public class FontFamily
{ {
static FontFamily()
{
Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
}
/// <inheritdoc /> /// <inheritdoc />
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T:Avalonia.Media.FontFamily" /> class. /// Initializes a new instance of the <see cref="T:Avalonia.Media.FontFamily" /> class.
@ -30,9 +34,7 @@ namespace Avalonia.Media
{ {
if (string.IsNullOrEmpty(name)) if (string.IsNullOrEmpty(name))
{ {
FamilyNames = new FamilyNameCollection(string.Empty); throw new ArgumentNullException(nameof(name));
return;
} }
var fontFamilySegment = GetFontFamilyIdentifier(name); var fontFamilySegment = GetFontFamilyIdentifier(name);
@ -53,13 +55,13 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Represents the default font family /// Represents the default font family
/// </summary> /// </summary>
public static FontFamily Default => new FontFamily(string.Empty); public static FontFamily Default { get; }
/// <summary> /// <summary>
/// Represents all font families in the system. This can be an expensive call depending on platform implementation. /// Represents all font families in the system. This can be an expensive call depending on platform implementation.
/// </summary> /// </summary>
public static IEnumerable<FontFamily> SystemFontFamilies => public static IEnumerable<FontFamily> SystemFontFamilies =>
AvaloniaLocator.Current.GetService<IPlatformRenderInterface>().InstalledFontNames.Select(name => new FontFamily(name)); FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
/// <summary> /// <summary>
/// Gets the primary family name of the font family. /// Gets the primary family name of the font family.
@ -181,7 +183,14 @@ namespace Avalonia.Media
{ {
var hash = (int)2186146271; var hash = (int)2186146271;
hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); if (Key != null)
{
hash = (hash * 15768619) ^ Key.GetHashCode();
}
else
{
hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
}
if (Key != null) if (Key != null)
{ {

100
src/Avalonia.Visuals/Media/FontManager.cs

@ -0,0 +1,100 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Platform;
namespace Avalonia.Media
{
public abstract class FontManager : IFontManagerImpl
{
public static readonly FontManager Default = CreateDefaultFontManger();
/// <inheritdoc cref="IFontManagerImpl"/>
public string DefaultFontFamilyName { get; protected set; }
private static FontManager CreateDefaultFontManger()
{
var platformImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
if(platformImpl == null)
{
return new EmptyFontManager();
}
return new PlatformFontManger(platformImpl);
}
/// <inheritdoc cref="IFontManagerImpl"/>
public abstract IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <inheritdoc cref="IFontManagerImpl"/>
public abstract IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
/// <inheritdoc cref="IFontManagerImpl"/>
public abstract Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
/// <inheritdoc cref="IFontManagerImpl"/>
public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
private class PlatformFontManger : FontManager
{
private readonly IFontManagerImpl _platformImpl;
public PlatformFontManger(IFontManagerImpl platformImpl)
{
_platformImpl = platformImpl;
DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
_platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) => _platformImpl.CreateGlyphTypeface(typeface);
public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) =>
_platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle);
public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null) =>
_platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture);
}
private class EmptyFontManager : FontManager
{
private readonly string[] _defaultFontFamilies = { "Arial" };
public EmptyFontManager()
{
DefaultFontFamilyName = "Arial";
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return _defaultFontFamilies;
}
public override IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
throw new NotSupportedException();
}
public override Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
throw new NotSupportedException();
}
public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
{
throw new NotSupportedException();
}
}
}
}

6
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@ -9,7 +9,7 @@ using System.Text;
namespace Avalonia.Media.Fonts namespace Avalonia.Media.Fonts
{ {
public class FamilyNameCollection : IEnumerable<string> public class FamilyNameCollection : IReadOnlyList<string>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FamilyNameCollection"/> class. /// Initializes a new instance of the <see cref="FamilyNameCollection"/> class.
@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts
return other.ToString().Equals(ToString()); return other.ToString().Equals(ToString());
} }
public int Count => Names.Count;
public string this[int index] => Names[index];
} }
} }

47
src/Avalonia.Visuals/Media/FormattedText.cs

@ -16,9 +16,10 @@ namespace Avalonia.Media
private IFormattedTextImpl _platformImpl; private IFormattedTextImpl _platformImpl;
private IReadOnlyList<FormattedTextStyleSpan> _spans; private IReadOnlyList<FormattedTextStyleSpan> _spans;
private Typeface _typeface; private Typeface _typeface;
private double _fontSize;
private string _text; private string _text;
private TextAlignment _textAlignment; private TextAlignment _textAlignment;
private TextWrapping _wrapping; private TextWrapping _textWrapping;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FormattedText"/> class. /// Initializes a new instance of the <see cref="FormattedText"/> class.
@ -37,6 +38,31 @@ namespace Avalonia.Media
_platform = platform; _platform = platform;
} }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="typeface"></param>
/// <param name="fontSize"></param>
/// <param name="textAlignment"></param>
/// <param name="textWrapping"></param>
/// <param name="constraint"></param>
public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
TextWrapping textWrapping, Size constraint)
{
_text = text;
_typeface = typeface;
_fontSize = fontSize;
_textAlignment = textAlignment;
_textWrapping = textWrapping;
_constraint = constraint;
}
/// <summary> /// <summary>
/// Gets the bounds of the text within the <see cref="Constraint"/>. /// Gets the bounds of the text within the <see cref="Constraint"/>.
/// </summary> /// </summary>
@ -61,6 +87,16 @@ namespace Avalonia.Media
set => Set(ref _typeface, value); set => Set(ref _typeface, value);
} }
/// <summary>
/// Gets or sets the font size.
/// </summary>
public double FontSize
{
get => _fontSize;
set => Set(ref _fontSize, value);
}
/// <summary> /// <summary>
/// Gets or sets a collection of spans that describe the formatting of subsections of the /// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text. /// text.
@ -92,10 +128,10 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the text wrapping. /// Gets or sets the text wrapping.
/// </summary> /// </summary>
public TextWrapping Wrapping public TextWrapping TextWrapping
{ {
get => _wrapping; get => _textWrapping;
set => Set(ref _wrapping, value); set => Set(ref _textWrapping, value);
} }
/// <summary> /// <summary>
@ -110,8 +146,9 @@ namespace Avalonia.Media
_platformImpl = _platform.CreateFormattedText( _platformImpl = _platform.CreateFormattedText(
_text, _text,
_typeface, _typeface,
_fontSize,
_textAlignment, _textAlignment,
_wrapping, _textWrapping,
_constraint, _constraint,
_spans); _spans);
} }

108
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@ -0,0 +1,108 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Platform;
namespace Avalonia.Media
{
public class GlyphTypeface : IDisposable
{
public GlyphTypeface(Typeface typeface) : this(FontManager.Default.CreateGlyphTypeface(typeface))
{
}
public GlyphTypeface(IGlyphTypefaceImpl platformImpl)
{
PlatformImpl = platformImpl;
}
public IGlyphTypefaceImpl PlatformImpl { get; }
/// <summary>
/// Gets the font design units per em.
/// </summary>
public short DesignEmHeight => PlatformImpl.DesignEmHeight;
/// <summary>
/// Gets the recommended distance above the baseline in design em size.
/// </summary>
public int Ascent => PlatformImpl.Ascent;
/// <summary>
/// Gets the recommended distance under the baseline in design em size.
/// </summary>
public int Descent => PlatformImpl.Descent;
/// <summary>
/// Gets the recommended additional space between two lines of text in design em size.
/// </summary>
public int LineGap => PlatformImpl.LineGap;
/// <summary>
/// Gets the recommended line height.
/// </summary>
public int LineHeight => Descent - Ascent + LineGap;
/// <summary>
/// Gets a value that indicates the distance of the underline from the baseline in design em size.
/// </summary>
public int UnderlinePosition => PlatformImpl.UnderlinePosition;
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
public int UnderlineThickness => PlatformImpl.UnderlineThickness;
/// <summary>
/// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
/// </summary>
public int StrikethroughPosition => PlatformImpl.StrikethroughPosition;
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>
/// <remarks>
/// Returns <c>0</c> if a glyph isn't found.
/// </remarks>
/// <param name="codepoint">The codepoint.</param>
/// <returns>
/// A glyph index.
/// </returns>
public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint);
/// <summary>
/// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
/// </summary>
/// <param name="codepoints">The codepoints to map.</param>
/// <returns></returns>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) => PlatformImpl.GetGlyphs(codepoints);
/// <summary>
/// Returns the glyph advance for the specified glyph.
/// </summary>
/// <param name="glyph">The glyph.</param>
/// <returns>
/// The advance.
/// </returns>
public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph);
/// <summary>
/// Returns an array of glyph advances in design em size.
/// </summary>
/// <param name="glyphs">The glyph indices.</param>
/// <returns></returns>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) => PlatformImpl.GetGlyphAdvances(glyphs);
void IDisposable.Dispose()
{
PlatformImpl?.Dispose();
}
}
}

99
src/Avalonia.Visuals/Media/Typeface.cs

@ -1,39 +1,38 @@
using System; // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Diagnostics;
using JetBrains.Annotations;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Represents a typeface. /// Represents a typeface.
/// </summary> /// </summary>
public class Typeface [DebuggerDisplay("Name = {FontFamily.Name}, Style = {Style}, Weight = {Weight}")]
public class Typeface : IEquatable<Typeface>
{ {
public static readonly Typeface Default = new Typeface(FontFamily.Default); public static readonly Typeface Default = new Typeface(FontFamily.Default);
private GlyphTypeface _glyphTypeface;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Typeface"/> class. /// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary> /// </summary>
/// <param name="fontFamily">The font family.</param> /// <param name="fontFamily">The font family.</param>
/// <param name="fontSize">The font size, in DIPs.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param> /// <param name="weight">The font weight.</param>
public Typeface( /// <param name="style">The font style.</param>
FontFamily fontFamily, public Typeface([NotNull]FontFamily fontFamily,
double fontSize = 12, FontWeight weight = FontWeight.Normal,
FontStyle style = FontStyle.Normal, FontStyle style = FontStyle.Normal)
FontWeight weight = FontWeight.Normal)
{ {
if (fontSize <= 0)
{
throw new ArgumentException("Font size must be > 0.");
}
if (weight <= 0) if (weight <= 0)
{ {
throw new ArgumentException("Font weight must be > 0."); throw new ArgumentException("Font weight must be > 0.");
} }
FontFamily = fontFamily; FontFamily = fontFamily;
FontSize = fontSize;
Style = style; Style = style;
Weight = weight; Weight = weight;
} }
@ -42,15 +41,12 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="Typeface"/> class. /// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary> /// </summary>
/// <param name="fontFamilyName">The name of the font family.</param> /// <param name="fontFamilyName">The name of the font family.</param>
/// <param name="fontSize">The font size, in DIPs.</param>
/// <param name="style">The font style.</param> /// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param> /// <param name="weight">The font weight.</param>
public Typeface( public Typeface(string fontFamilyName,
string fontFamilyName, FontWeight weight = FontWeight.Normal,
double fontSize = 12, FontStyle style = FontStyle.Normal)
FontStyle style = FontStyle.Normal, : this(new FontFamily(fontFamilyName), weight, style)
FontWeight weight = FontWeight.Normal)
: this(new FontFamily(fontFamilyName), fontSize, style, weight)
{ {
} }
@ -59,11 +55,6 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public FontFamily FontFamily { get; } public FontFamily FontFamily { get; }
/// <summary>
/// Gets the size of the font in DIPs.
/// </summary>
public double FontSize { get; }
/// <summary> /// <summary>
/// Gets the font style. /// Gets the font style.
/// </summary> /// </summary>
@ -73,5 +64,59 @@ namespace Avalonia.Media
/// Gets the font weight. /// Gets the font weight.
/// </summary> /// </summary>
public FontWeight Weight { get; } public FontWeight Weight { get; }
/// <summary>
/// Gets the glyph typeface.
/// </summary>
/// <value>
/// The glyph typeface.
/// </value>
public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this));
public static bool operator !=(Typeface a, Typeface b)
{
return !(a == b);
}
public static bool operator ==(Typeface a, Typeface b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
public override bool Equals(object obj)
{
if (obj is Typeface typeface)
{
return Equals(typeface);
}
return false;
}
public bool Equals(Typeface other)
{
if (other is null)
{
return false;
}
return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight;
}
public override int GetHashCode()
{
unchecked
{
var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (int)Style;
hashCode = (hashCode * 397) ^ (int)Weight;
return hashCode;
}
}
} }
} }

57
src/Avalonia.Visuals/Platform/IFontManagerImpl.cs

@ -0,0 +1,57 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
namespace Avalonia.Platform
{
public interface IFontManagerImpl
{
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
string DefaultFontFamilyName { get; }
/// <summary>
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <summary>
/// Creates a glyph typeface for specified typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>
/// The glyph typeface implementation.
/// </returns>
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
/// <summary>
/// Get a typeface from specified parameters.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The typeface.
/// </returns>
Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
/// </summary>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param>
/// <returns>
/// The typeface.
/// </returns>
Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
}
}

89
src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs

@ -0,0 +1,89 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Platform
{
public interface IGlyphTypefaceImpl : IDisposable
{
/// <summary>
/// Gets the font design units per em.
/// </summary>
short DesignEmHeight { get; }
/// <summary>
/// Gets the recommended distance above the baseline in design em size.
/// </summary>
int Ascent { get; }
/// <summary>
/// Gets the recommended distance under the baseline in design em size.
/// </summary>
int Descent { get; }
/// <summary>
/// Gets the recommended additional space between two lines of text in design em size.
/// </summary>
int LineGap { get; }
/// <summary>
/// Gets a value that indicates the distance of the underline from the baseline in design em size.
/// </summary>
int UnderlinePosition { get; }
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
int UnderlineThickness { get; }
/// <summary>
/// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
/// </summary>
int StrikethroughPosition { get; }
/// <summary>
/// Gets a value that indicates the thickness of the underline in design em size.
/// </summary>
int StrikethroughThickness { get; }
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>
/// <remarks>
/// Returns <c>0</c> if a glyph isn't found.
/// </remarks>
/// <param name="codepoint">The codepoint.</param>
/// <returns>
/// A glyph index.
/// </returns>
ushort GetGlyph(uint codepoint);
/// <summary>
/// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as <code>0</code>.
/// </summary>
/// <param name="codepoints">The codepoints to map.</param>
/// <returns>
/// An array of glyph indices.
/// </returns>
ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints);
/// <summary>
/// Returns the glyph advance for the specified glyph.
/// </summary>
/// <param name="glyph">The glyph.</param>
/// <returns>
/// The advance.
/// </returns>
int GetGlyphAdvance(ushort glyph);
/// <summary>
/// Returns an array of glyph advances in design em size.
/// </summary>
/// <param name="glyphs">The glyph indices.</param>
/// <returns>
/// An array of glyph advances.
/// </returns>
int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs);
}
}

7
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -13,16 +13,12 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
public interface IPlatformRenderInterface public interface IPlatformRenderInterface
{ {
/// <summary>
/// Get all installed fonts in the system
/// </summary>
IEnumerable<string> InstalledFontNames { get; }
/// <summary> /// <summary>
/// Creates a formatted text implementation. /// Creates a formatted text implementation.
/// </summary> /// </summary>
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
/// <param name="typeface">The base typeface.</param> /// <param name="typeface">The base typeface.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="textAlignment">The text alignment.</param> /// <param name="textAlignment">The text alignment.</param>
/// <param name="wrapping">The text wrapping mode.</param> /// <param name="wrapping">The text wrapping mode.</param>
/// <param name="constraint">The text layout constraints.</param> /// <param name="constraint">The text layout constraints.</param>
@ -31,6 +27,7 @@ namespace Avalonia.Platform
IFormattedTextImpl CreateFormattedText( IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,

6
src/Avalonia.Visuals/Rendering/RendererBase.cs

@ -7,7 +7,8 @@ namespace Avalonia.Rendering
{ {
public class RendererBase public class RendererBase
{ {
private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18); private static readonly Typeface s_fpsTypeface = new Typeface("Arial");
private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond; private int _framesThisSecond;
private int _fps; private int _fps;
@ -18,7 +19,8 @@ namespace Avalonia.Rendering
{ {
_fpsText = new FormattedText _fpsText = new FormattedText
{ {
Typeface = s_fpsTypeface Typeface = s_fpsTypeface,
FontSize = s_fontSize
}; };
} }

1
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@ -12,5 +12,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\SkiaSharp.props" /> <Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" /> <Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
</Project> </Project>

40
src/Skia/Avalonia.Skia/FontKey.cs

@ -0,0 +1,40 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
namespace Avalonia.Skia
{
internal readonly struct FontKey : IEquatable<FontKey>
{
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontWeight weight, FontStyle style)
{
Style = style;
Weight = weight;
}
public override int GetHashCode()
{
var hash = 17;
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
return hash;
}
public override bool Equals(object other)
{
return other is FontKey key && Equals(key);
}
public bool Equals(FontKey other)
{
return Style == other.Style &&
Weight == other.Weight;
}
}
}

91
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -0,0 +1,91 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class FontManagerImpl : IFontManagerImpl
{
private SKFontManager _skFontManager = SKFontManager.Default;
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
public FontManagerImpl()
{
DefaultFontFamilyName = SKTypeface.Default.FamilyName;
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
{
_skFontManager = SKFontManager.CreateDefault();
}
return _skFontManager.FontFamilies;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
}
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
{
var fontFamilyName = FontFamily.Default.Name;
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
if (fontFamily != null)
{
foreach (var familyName in fontFamily.FamilyNames)
{
var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
if (skTypeface == null)
{
continue;
}
fontFamilyName = familyName;
break;
}
}
else
{
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
if (skTypeface != null)
{
fontFamilyName = skTypeface.FamilyName;
}
}
return GetTypeface(fontFamilyName, fontWeight, fontStyle);
}
}
}

48
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -18,6 +18,7 @@ namespace Avalonia.Skia
public FormattedTextImpl( public FormattedTextImpl(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -28,47 +29,22 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B) // Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B); Text = Text.Replace((char)0, (char)0x200B);
SKTypeface skiaTypeface = null; var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
if (typeface.FontFamily.Key != null) _paint = new SKPaint
{ {
var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); TextEncoding = SKTextEncoding.Utf16,
skiaTypeface = typefaces.GetTypeFace(typeface); IsStroke = false,
} IsAntialias = true,
else LcdRenderText = true,
{ SubpixelText = true,
if (typeface.FontFamily.FamilyNames.HasFallbacks) Typeface = entry.SKTypeface,
{ TextSize = (float)fontSize,
foreach (var familyName in typeface.FontFamily.FamilyNames) TextAlign = textAlignment.ToSKTextAlign()
{ };
skiaTypeface = TypefaceCache.GetTypeface(
familyName,
typeface.Style,
typeface.Weight);
if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break;
}
}
else
{
skiaTypeface = TypefaceCache.GetTypeface(
typeface.FontFamily.Name,
typeface.Style,
typeface.Weight);
}
}
_paint = new SKPaint();
//currently Skia does not measure properly with Utf8 !!! //currently Skia does not measure properly with Utf8 !!!
//Paint.TextEncoding = SKTextEncoding.Utf8; //Paint.TextEncoding = SKTextEncoding.Utf8;
_paint.TextEncoding = SKTextEncoding.Utf16;
_paint.IsStroke = false;
_paint.IsAntialias = true;
_paint.LcdRenderText = true;
_paint.SubpixelText = true;
_paint.Typeface = skiaTypeface;
_paint.TextSize = (float)typeface.FontSize;
_paint.TextAlign = textAlignment.ToSKTextAlign();
_wrapping = wrapping; _wrapping = wrapping;
_constraint = constraint; _constraint = constraint;

179
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -0,0 +1,179 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Platform;
using HarfBuzzSharp;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class GlyphTypefaceImpl : IGlyphTypefaceImpl
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
{
Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
Face = new Face(GetTable)
{
UnitsPerEm = Typeface.UnitsPerEm
};
Font = new Font(Face);
Font.SetFunctionsOpenType();
Font.GetScale(out var xScale, out _);
DesignEmHeight = (short)xScale;
if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
{
Font.TryGetVerticalFontExtents(out fontExtents);
}
Ascent = -fontExtents.Ascender;
Descent = -fontExtents.Descender;
LineGap = fontExtents.LineGap;
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
{
UnderlinePosition = underlinePosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
{
UnderlineThickness = underlineThickness;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
{
StrikethroughPosition = strikethroughPosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
{
StrikethroughThickness = strikethroughThickness;
}
}
public Face Face { get; }
public Font Font { get; }
public SKTypeface Typeface { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public short DesignEmHeight { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Ascent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Descent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
//ToDo: Get these values from HarfBuzz
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlineThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughPosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{
if (Font.TryGetGlyph(codepoint, out var glyph))
{
return (ushort)glyph;
}
return 0;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var glyphs = new ushort[codepoints.Length];
for (var i = 0; i < codepoints.Length; i++)
{
if (Font.TryGetGlyph(codepoints[i], out var glyph))
{
glyphs[i] = (ushort)glyph;
}
}
return glyphs;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int GetGlyphAdvance(ushort glyph)
{
return Font.GetHorizontalGlyphAdvance(glyph);
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var glyphIndices = new uint[glyphs.Length];
for (var i = 0; i < glyphs.Length; i++)
{
glyphIndices[i] = glyphs[i];
}
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
private Blob GetTable(Face face, Tag tag)
{
var size = Typeface.GetTableSize(tag);
var data = Marshal.AllocCoTaskMem(size);
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
return Typeface.TryGetTableData(tag, 0, size, data) ?
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
}
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font?.Dispose();
Face?.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

5
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -21,8 +21,6 @@ namespace Avalonia.Skia
private GRContext GrContext { get; } private GRContext GrContext { get; }
public IEnumerable<string> InstalledFontNames => SKFontManager.Default.FontFamilies;
public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
{ {
if (customSkiaGpu != null) if (customSkiaGpu != null)
@ -52,12 +50,13 @@ namespace Avalonia.Skia
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans) IReadOnlyList<FormattedTextStyleSpan> spans)
{ {
return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans); return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
} }
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);

91
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -4,114 +4,59 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
internal class SKTypefaceCollection internal class SKTypefaceCollection
{ {
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>> _fontFamilies = private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> _fontFamilies =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, SKTypeface>>(); new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
public void AddTypeFace(SKTypeface typeface) public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry)
{ {
var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); if (!_fontFamilies.TryGetValue(familyName, out var fontFamily))
if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily))
{ {
fontFamily = new ConcurrentDictionary<FontKey, SKTypeface>(); fontFamily = new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>();
_fontFamilies.TryAdd(typeface.FamilyName, fontFamily); _fontFamilies.TryAdd(familyName, fontFamily);
} }
fontFamily.TryAdd(key, typeface); fontFamily.TryAdd(key, entry);
} }
public SKTypeface GetTypeFace(Typeface typeface) public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle)
{ {
var styleSlant = SKFontStyleSlant.Upright; var key = new FontKey(fontWeight, fontStyle);
switch (typeface.Style)
{
case FontStyle.Italic:
styleSlant = SKFontStyleSlant.Italic;
break;
case FontStyle.Oblique:
styleSlant = SKFontStyleSlant.Oblique;
break;
}
if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily)) return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
{ fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight); null;
}
var weight = (SKFontStyleWeight)typeface.Weight;
var key = new FontKey(weight, styleSlant);
return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key));
} }
private static SKTypeface GetFallback(IDictionary<FontKey, SKTypeface> fontFamily, FontKey key) private static TypefaceCollectionEntry GetFallback(IDictionary<FontKey, TypefaceCollectionEntry> fontFamily, FontKey key)
{ {
var keys = fontFamily.Keys.Where( var keys = fontFamily.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray(); x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray();
if (!keys.Any()) if (!keys.Any())
{ {
keys = fontFamily.Keys.Where( keys = fontFamily.Keys.Where(
x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
if (!keys.Any()) if (!keys.Any())
{ {
keys = fontFamily.Keys.Where( keys = fontFamily.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
(x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray(); (x.Style >= key.Style || x.Style < key.Style)).ToArray();
} }
} }
key = keys.FirstOrDefault(); key = keys.FirstOrDefault();
fontFamily.TryGetValue(key, out var typeface); fontFamily.TryGetValue(key, out var entry);
return typeface;
}
private struct FontKey
{
public readonly SKFontStyleSlant Slant;
public readonly SKFontStyleWeight Weight;
public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
{
Slant = slant;
Weight = weight;
}
public override int GetHashCode()
{
var hash = 17;
hash = (hash * 31) + (int)Slant;
hash = (hash * 31) + (int)Weight;
return hash;
}
public override bool Equals(object other) return entry;
{
return other is FontKey key && this.Equals(key);
}
private bool Equals(FontKey other)
{
return Slant == other.Slant &&
Weight == other.Weight;
}
} }
} }
} }

8
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -45,9 +45,13 @@ namespace Avalonia.Skia
{ {
var assetStream = assetLoader.Open(asset); var assetStream = assetLoader.Open(asset);
var typeface = SKTypeface.FromStream(assetStream); var skTypeface = SKTypeface.FromStream(assetStream);
typeFaceCollection.AddTypeFace(typeface); var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant);
var entry = new TypefaceCollectionEntry(typeface, skTypeface);
typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry);
} }
return typeFaceCollection; return typeFaceCollection;

5
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@ -25,6 +25,11 @@ namespace Avalonia.Skia
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface); .Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
var fontManager = new FontManagerImpl();
AvaloniaLocator.CurrentMutable
.Bind<IFontManagerImpl>().ToConstant(fontManager);
} }
/// <summary> /// <summary>

86
src/Skia/Avalonia.Skia/TypefaceCache.cs

@ -1,7 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic; using System.Collections.Concurrent;
using Avalonia.Media; using Avalonia.Media;
using SkiaSharp; using SkiaSharp;
@ -12,88 +12,36 @@ namespace Avalonia.Skia
/// </summary> /// </summary>
internal static class TypefaceCache internal static class TypefaceCache
{ {
public static readonly string DefaultFamilyName = CreateDefaultFamilyName(); private static readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> s_cache =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
private static readonly Dictionary<string, Dictionary<FontKey, SKTypeface>> s_cache = public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
new Dictionary<string, Dictionary<FontKey, SKTypeface>>();
struct FontKey
{ {
public readonly SKFontStyleSlant Slant; if (fontFamily.Key != null)
public readonly SKFontStyleWeight Weight;
public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
{ {
Slant = slant; return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
Weight = weight; .Get(fontFamily.Name, fontWeight, fontStyle);
} }
public override int GetHashCode() var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>());
{
int hash = 17;
hash = hash * 31 + (int)Slant;
hash = hash * 31 + (int)Weight;
return hash;
}
public override bool Equals(object other)
{
return other is FontKey ? Equals((FontKey)other) : false;
}
public bool Equals(FontKey other)
{
return Slant == other.Slant &&
Weight == other.Weight;
}
// Equals and GetHashCode ommitted
}
private static string CreateDefaultFamilyName()
{
var defaultTypeface = SKTypeface.CreateDefault();
return defaultTypeface.FamilyName; var key = new FontKey(fontWeight, fontStyle);
}
private static SKTypeface GetTypeface(string name, FontKey key) if (typefaceCollection.TryGetValue(key, out var entry))
{
var familyKey = name;
if (!s_cache.TryGetValue(familyKey, out var entry))
{ {
s_cache[familyKey] = entry = new Dictionary<FontKey, SKTypeface>(); return entry;
} }
if (!entry.TryGetValue(key, out var typeface)) var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
{ SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ??
GetTypeface(DefaultFamilyName, key);
entry[key] = typeface; var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
}
return typeface; entry = new TypefaceCollectionEntry(typeface, skTypeface);
}
public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight)
{
var skStyle = SKFontStyleSlant.Upright;
switch (style) typefaceCollection[key] = entry;
{
case FontStyle.Italic:
skStyle = SKFontStyleSlant.Italic;
break;
case FontStyle.Oblique:
skStyle = SKFontStyleSlant.Oblique;
break;
}
return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle)); return entry;
} }
} }
} }

19
src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs

@ -0,0 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class TypefaceCollectionEntry
{
public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface)
{
Typeface = typeface;
SKTypeface = skTypeface;
}
public Typeface Typeface { get; }
public SKTypeface SKTypeface { get; }
}
}

1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@ -14,6 +14,7 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\Rx.props" /> <Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\SharpDX.props" /> <Import Project="..\..\..\build\SharpDX.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" /> <Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
<Import Project="..\..\..\build\JetBrains.Annotations.props" /> <Import Project="..\..\..\build\JetBrains.Annotations.props" />
</Project> </Project>

17
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -41,20 +41,6 @@ namespace Avalonia.Direct2D1
public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; } public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; }
public IEnumerable<string> InstalledFontNames
{
get
{
var cache = Direct2D1FontCollectionCache.s_installedFontCollection;
var length = cache.FontFamilyCount;
for (int i = 0; i < length; i++)
{
var names = cache.GetFontFamily(i).FamilyNames;
yield return names.GetString(0);
}
}
}
private static readonly object s_initLock = new object(); private static readonly object s_initLock = new object();
private static bool s_initialized = false; private static bool s_initialized = false;
@ -120,6 +106,7 @@ namespace Avalonia.Direct2D1
{ {
InitializeDirect2D(); InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance); AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance);
AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true; SharpDX.Configuration.EnableReleaseOnFinalizer = true;
} }
@ -131,6 +118,7 @@ namespace Avalonia.Direct2D1
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -139,6 +127,7 @@ namespace Avalonia.Direct2D1
return new FormattedTextImpl( return new FormattedTextImpl(
text, text,
typeface, typeface,
fontSize,
textAlignment, textAlignment,
wrapping, wrapping,
constraint, constraint,

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

@ -1,62 +1,61 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts; using Avalonia.Media.Fonts;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = SharpDX.DirectWrite.FontStyle;
using FontWeight = SharpDX.DirectWrite.FontWeight;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
internal static class Direct2D1FontCollectionCache internal static class Direct2D1FontCollectionCache
{ {
private static readonly ConcurrentDictionary<FontFamilyKey, SharpDX.DirectWrite.FontCollection> s_cachedCollections; private static readonly ConcurrentDictionary<FontFamilyKey, FontCollection> s_cachedCollections;
internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection; internal static readonly FontCollection InstalledFontCollection;
static Direct2D1FontCollectionCache() static Direct2D1FontCollectionCache()
{ {
s_cachedCollections = new ConcurrentDictionary<FontFamilyKey, SharpDX.DirectWrite.FontCollection>(); s_cachedCollections = new ConcurrentDictionary<FontFamilyKey, FontCollection>();
s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false); InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
} }
public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface) public static Font GetFont(Typeface typeface)
{ {
var fontFamily = typeface.FontFamily; var fontFamily = typeface.FontFamily;
var fontCollection = GetOrAddFontCollection(fontFamily); var fontCollection = GetOrAddFontCollection(fontFamily);
var fontFamilyName = FontFamily.Default.Name;
// Should this be cached?
foreach (var familyName in fontFamily.FamilyNames) foreach (var familyName in fontFamily.FamilyNames)
{ {
if (!fontCollection.FindFamilyName(familyName, out _)) if (fontCollection.FindFamilyName(familyName, out var index))
{ {
continue; return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
FontStretch.Normal,
(FontStyle)typeface.Style);
} }
fontFamilyName = familyName;
break;
} }
return new SharpDX.DirectWrite.TextFormat( InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
Direct2D1Platform.DirectWriteFactory,
fontFamilyName, return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
fontCollection, (FontWeight)typeface.Weight,
(SharpDX.DirectWrite.FontWeight)typeface.Weight, FontStretch.Normal,
(SharpDX.DirectWrite.FontStyle)typeface.Style, (FontStyle)typeface.Style);
SharpDX.DirectWrite.FontStretch.Normal,
(float)typeface.FontSize);
} }
private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily) private static FontCollection GetOrAddFontCollection(FontFamily fontFamily)
{ {
return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
} }
private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key) private static FontCollection CreateFontCollection(FontFamilyKey key)
{ {
var assets = FontFamilyLoader.LoadFontAssets(key); var assets = FontFamilyLoader.LoadFontAssets(key);
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets); var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key); return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
} }
} }
} }

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

@ -0,0 +1,80 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
using FontStyle = Avalonia.Media.FontStyle;
using FontWeight = Avalonia.Media.FontWeight;
namespace Avalonia.Direct2D1.Media
{
internal class FontManagerImpl : IFontManagerImpl
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
public FontManagerImpl()
{
//ToDo: Implement a real lookup of the system's default font.
DefaultFontFamilyName = "segoe ui";
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
var fontFamilies = new string[familyCount];
for (var i = 0; i < familyCount; i++)
{
fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0);
}
return fontFamilies;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
//ToDo: Implement caching.
return new Typeface(fontFamily, fontWeight, fontStyle);
}
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
{
var fontFamilyName = FontFamily.Default.Name;
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
for (var i = 0; i < familyCount; i++)
{
var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i)
.GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal,
(SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0);
if (!font.HasCharacter(codepoint))
{
continue;
}
fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
break;
}
return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
}
}
}

19
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@ -14,6 +14,7 @@ namespace Avalonia.Direct2D1.Media
public FormattedTextImpl( public FormattedTextImpl(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,
@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media
{ {
Text = text; Text = text;
using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface)) using (var font = Direct2D1FontCollectionCache.GetFont(typeface))
using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory,
typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight,
(DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize))
{ {
textFormat.WordWrapping = textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout( TextLayout = new DWrite.TextLayout(
Direct2D1Platform.DirectWriteFactory, Direct2D1Platform.DirectWriteFactory,
Text ?? string.Empty, Text ?? string.Empty,
textFormat, textFormat,
(float)constraint.Width, (float)constraint.Width,
(float)constraint.Height) (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() };
{
TextAlignment = textAlignment.ToDirect2D()
};
} }
if (spans != null) if (spans != null)

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

@ -0,0 +1,188 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
using Avalonia.Platform;
using HarfBuzzSharp;
using SharpDX.DirectWrite;
namespace Avalonia.Direct2D1.Media
{
internal class GlyphTypefaceImpl : IGlyphTypefaceImpl
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
FontFace = new FontFace(DWFont);
Face = new Face(GetTable);
Font = new HarfBuzzSharp.Font(Face);
Font.SetFunctionsOpenType();
Font.GetScale(out var xScale, out _);
DesignEmHeight = (short)xScale;
if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
{
Font.TryGetVerticalFontExtents(out fontExtents);
}
Ascent = -fontExtents.Ascender;
Descent = -fontExtents.Descender;
LineGap = fontExtents.LineGap;
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
{
UnderlinePosition = underlinePosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
{
UnderlineThickness = underlineThickness;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
{
StrikethroughPosition = strikethroughPosition;
}
if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
{
StrikethroughThickness = strikethroughThickness;
}
}
private Blob GetTable(Face face, Tag tag)
{
var dwTag = (int)SwapBytes(tag);
if (FontFace.TryGetFontTable(dwTag, out var tableData, out _))
{
return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { });
}
return null;
}
private static uint SwapBytes(uint x)
{
x = (x >> 16) | (x << 16);
return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8);
}
public SharpDX.DirectWrite.Font DWFont { get; }
public FontFace FontFace { get; }
public Face Face { get; }
public HarfBuzzSharp.Font Font { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public short DesignEmHeight { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Ascent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int Descent { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
//ToDo: Read font table for these values
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlineThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughPosition { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{
if (Font.TryGetGlyph(codepoint, out var glyph))
{
return (ushort)glyph;
}
return 0;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
var glyphs = new ushort[codepoints.Length];
for (var i = 0; i < codepoints.Length; i++)
{
if (Font.TryGetGlyph(codepoints[i], out var glyph))
{
glyphs[i] = (ushort)glyph;
}
}
return glyphs;
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int GetGlyphAdvance(ushort glyph)
{
return Font.GetHorizontalGlyphAdvance(glyph);
}
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var glyphIndices = new uint[glyphs.Length];
for (var i = 0; i < glyphs.Length; i++)
{
glyphIndices[i] = glyphs[i];
}
return Font.GetHorizontalGlyphAdvances(glyphIndices);
}
private void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!disposing)
{
return;
}
Font?.Dispose();
Face?.Dispose();
FontFace?.Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

1
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@ -175,6 +175,7 @@ namespace Avalonia.Layout.UnitTests
x.CreateFormattedText( x.CreateFormattedText(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Typeface>(), It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(), It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(), It.IsAny<TextWrapping>(),
It.IsAny<Size>(), It.IsAny<Size>(),

3
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@ -53,7 +53,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{ {
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text, return r.CreateFormattedText(text,
new Typeface(fontFamily, fontSize, fontStyle, fontWeight), new Typeface(fontFamily, fontWeight, fontStyle),
fontSize,
textAlignment, textAlignment,
wrapping, wrapping,
widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity), widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),

3
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -9,11 +9,10 @@ namespace Avalonia.UnitTests
{ {
public class MockPlatformRenderInterface : IPlatformRenderInterface public class MockPlatformRenderInterface : IPlatformRenderInterface
{ {
public IEnumerable<string> InstalledFontNames => new string[0];
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,

1
tests/Avalonia.UnitTests/TestServices.cs

@ -169,6 +169,7 @@ namespace Avalonia.UnitTests
x.CreateFormattedText( x.CreateFormattedText(
It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Typeface>(), It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(), It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(), It.IsAny<TextWrapping>(),
It.IsAny<Size>(), It.IsAny<Size>(),

44
tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs

@ -19,12 +19,48 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(new FontFamily("Arial"), fontFamily); Assert.Equal(new FontFamily("Arial"), fontFamily);
} }
[Fact] [InlineData("Font A")]
public void Should_Be_Equal() [InlineData("Font A, Font B")]
[InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")]
[InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")]
[Theory]
public void Should_Have_Equal_Hash(string s)
{ {
var fontFamily = new FontFamily("Arial"); var fontFamily = new FontFamily(s);
Assert.Equal(new FontFamily("Arial"), fontFamily); Assert.Equal(new FontFamily(s).GetHashCode(), fontFamily.GetHashCode());
}
[InlineData("Font A, Font B", "Font B, Font A")]
[InlineData("Font A, Font B", "Font A, Font C")]
[Theory]
public void Should_Not_Have_Equal_Hash(string a, string b)
{
var fontFamily = new FontFamily(b);
Assert.NotEqual(new FontFamily(a).GetHashCode(), fontFamily.GetHashCode());
}
[InlineData("Font A")]
[InlineData("Font A, Font B")]
[InlineData("resm: Avalonia.Visuals.UnitTests#MyFont")]
[InlineData("avares://Avalonia.Visuals.UnitTests/Assets/Fonts#MyFont")]
[Theory]
public void Should_Be_Equal(string s)
{
var fontFamily = new FontFamily(s);
Assert.Equal(new FontFamily(s), fontFamily);
}
[InlineData("Font A, Font B", "Font B, Font A")]
[InlineData("Font A, Font B", "Font A, Font C")]
[Theory]
public void Should_Not_Be_Equal(string a, string b)
{
var fontFamily = new FontFamily(b);
Assert.NotEqual(new FontFamily(a), fontFamily);
} }
[Fact] [Fact]

14
tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs

@ -7,15 +7,21 @@ namespace Avalonia.Visuals.UnitTests.Media
public class TypefaceTests public class TypefaceTests
{ {
[Fact] [Fact]
public void Exception_Should_Be_Thrown_If_FontSize_LessThanEqualTo_0() public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero()
{ {
Assert.Throws<ArgumentException>(() => new Typeface("foo", 0)); Assert.Throws<ArgumentException>(() => new Typeface("foo", 0, (FontStyle)12));
} }
[Fact] [Fact]
public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_0() public void Should_Be_Equal()
{ {
Assert.Throws<ArgumentException>(() => new Typeface("foo", 12, weight: 0)); Assert.Equal(new Typeface("Font A"), new Typeface("Font A"));
}
[Fact]
public void Should_Have_Equal_Hash()
{
Assert.Equal(new Typeface("Font A").GetHashCode(), new Typeface("Font A").GetHashCode());
} }
} }
} }

1
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -13,6 +13,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
public IFormattedTextImpl CreateFormattedText( public IFormattedTextImpl CreateFormattedText(
string text, string text,
Typeface typeface, Typeface typeface,
double fontSize,
TextAlignment textAlignment, TextAlignment textAlignment,
TextWrapping wrapping, TextWrapping wrapping,
Size constraint, Size constraint,

Loading…
Cancel
Save