Browse Source

Merge branch 'master' into fixes/3323-resourcedictionary-resource

pull/3327/head
Jumar Macato 6 years ago
committed by GitHub
parent
commit
01c1669e7b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      build/HarfBuzzSharp.props
  2. 4
      build/SkiaSharp.props
  3. 4
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  4. 3
      samples/RenderDemo/MainWindow.xaml
  5. 14
      samples/RenderDemo/Pages/GlyphRunPage.xaml
  6. 80
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  7. 48
      src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
  8. 4
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  9. 2
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  10. 2
      src/Avalonia.Controls/TextBlock.cs
  11. 18
      src/Avalonia.Input/FocusManager.cs
  12. 2
      src/Avalonia.Visuals/Matrix.cs
  13. 68
      src/Avalonia.Visuals/Media/CharacterHit.cs
  14. 16
      src/Avalonia.Visuals/Media/DrawingContext.cs
  15. 38
      src/Avalonia.Visuals/Media/FontFamily.cs
  16. 153
      src/Avalonia.Visuals/Media/FontManager.cs
  17. 38
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  18. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
  19. 15
      src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
  20. 15
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  21. 457
      src/Avalonia.Visuals/Media/GlyphRun.cs
  22. 50
      src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
  23. 11
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  24. 19
      src/Avalonia.Visuals/Media/Transform.cs
  25. 4
      src/Avalonia.Visuals/Media/Typeface.cs
  26. 8
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  27. 30
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  28. 12
      src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
  29. 5
      src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
  30. 17
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  31. 2
      src/Avalonia.Visuals/Point.cs
  32. 11
      src/Avalonia.Visuals/RelativePoint.cs
  33. 3
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  34. 15
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  35. 91
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  36. 154
      src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
  37. 21
      src/Avalonia.Visuals/Vector.cs
  38. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
  39. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
  40. 14
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  41. 77
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  42. 4
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  43. 35
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  44. 10
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  45. 96
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  46. 4
      src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
  47. 47
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  48. 8
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  49. 5
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  50. 47
      src/Skia/Avalonia.Skia/TypefaceCache.cs
  51. 19
      src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
  52. 58
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  53. 9
      src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
  54. 16
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  55. 34
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  56. 14
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  57. 19
      src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
  58. 9
      src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
  59. 4
      src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
  60. 44
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  61. 111
      tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
  62. 2
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  63. 23
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  64. 52
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  65. 2
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  66. 93
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  67. BIN
      tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf
  68. 5
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  69. 35
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  70. 47
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs
  71. 10
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  72. 25
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  73. 130
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
  74. 10
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

4
build/HarfBuzzSharp.props

@ -1,6 +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" />
<PackageReference Include="HarfBuzzSharp" Version="2.6.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1" />
</ItemGroup>
</Project>

4
build/SkiaSharp.props

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

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

@ -1,5 +1,7 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace ControlCatalog.Pages
{
@ -14,7 +16,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Find<ComboBox>("fontComboBox");
fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies;
fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.SelectedIndex = 0;
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -41,6 +41,9 @@
<TabItem Header="RenderTargetBitmap">
<pages:RenderTargetBitmapPage/>
</TabItem>
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

14
samples/RenderDemo/Pages/GlyphRunPage.xaml

@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="RenderDemo.Pages.GlyphRunPage">
<Border
Background="White">
<DrawingPresenter
x:Name="drawingPresenter"
Stretch="None">
</DrawingPresenter>
</Border>
</UserControl>

80
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -0,0 +1,80 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
namespace RenderDemo.Pages
{
public class GlyphRunPage : UserControl
{
private DrawingPresenter _drawingPresenter;
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
private float _fontSize = 20;
private int _direction = 10;
public GlyphRunPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_drawingPresenter = this.FindControl<DrawingPresenter>("drawingPresenter");
DispatcherTimer.Run(() =>
{
UpdateGlyphRun();
return true;
}, TimeSpan.FromSeconds(1));
}
private void UpdateGlyphRun()
{
var c = (uint)_rand.Next(65, 90);
if (_fontSize + _direction > 200)
{
_direction = -10;
}
if (_fontSize + _direction < 20)
{
_direction = 10;
}
_fontSize += _direction;
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
var drawingGroup = new DrawingGroup();
var glyphRunDrawing = new GlyphRunDrawing
{
Foreground = Brushes.Black,
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale)
};
drawingGroup.Children.Add(glyphRunDrawing);
var geometryDrawing = new GeometryDrawing
{
Pen = new Pen(Brushes.Black),
Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds }
};
drawingGroup.Children.Add(geometryDrawing);
_drawingPresenter.Drawing = drawingGroup;
}
}
}

48
src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs

@ -0,0 +1,48 @@
using System.Collections;
using System.Collections.Generic;
namespace Avalonia.Utilities
{
public struct ImmutableReadOnlyListStructEnumerator<T> : IEnumerator, IEnumerator<T>
{
private readonly IReadOnlyList<T> _readOnlyList;
private int _pos;
public ImmutableReadOnlyListStructEnumerator(IReadOnlyList<T> readOnlyList)
{
_readOnlyList = readOnlyList;
_pos = -1;
Current = default;
}
public T Current
{
get;
private set;
}
object IEnumerator.Current => Current;
public void Dispose() { }
public bool MoveNext()
{
if (_pos >= _readOnlyList.Count - 1)
{
return false;
}
Current = _readOnlyList[++_pos];
return true;
}
public void Reset()
{
_pos = -1;
Current = default;
}
}
}

4
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -188,8 +188,8 @@ namespace Avalonia.Controls.Presenters
break;
case NotifyCollectionChangedAction.Remove:
if (e.OldStartingIndex >= FirstIndex &&
e.OldStartingIndex < NextIndex)
if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) ||
panel.Children.Count > ItemCount)
{
RecycleContainersOnRemove();
}

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

@ -297,7 +297,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText
{
Text = "X",
Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
TextAlignment = TextAlignment,
Constraint = availableSize,

2
src/Avalonia.Controls/TextBlock.cs

@ -352,7 +352,7 @@ namespace Avalonia.Controls
return new FormattedText
{
Constraint = constraint,
Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,

18
src/Avalonia.Input/FocusManager.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -17,8 +18,8 @@ namespace Avalonia.Input
/// <summary>
/// The focus scopes in which the focus is currently defined.
/// </summary>
private readonly Dictionary<IFocusScope, IInputElement> _focusScopes =
new Dictionary<IFocusScope, IInputElement>();
private readonly ConditionalWeakTable<IFocusScope, IInputElement> _focusScopes =
new ConditionalWeakTable<IFocusScope, IInputElement>();
/// <summary>
/// Initializes a new instance of the <see cref="FocusManager"/> class.
@ -110,7 +111,18 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(scope != null);
_focusScopes[scope] = element;
if (_focusScopes.TryGetValue(scope, out IInputElement existingElement))
{
if (element != existingElement)
{
_focusScopes.Remove(scope);
_focusScopes.Add(scope, element);
}
}
else
{
_focusScopes.Add(scope, element);
}
if (Scope == scope)
{

2
src/Avalonia.Visuals/Matrix.cs

@ -306,7 +306,7 @@ namespace Avalonia
/// <summary>
/// Parses a <see cref="Matrix"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <param name="s">Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new <see cref="Matrix"/></param>
/// <returns>The <see cref="Matrix"/>.</returns>
public static Matrix Parse(string s)
{

68
src/Avalonia.Visuals/Media/CharacterHit.cs

@ -0,0 +1,68 @@
// 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.Media
{
/// <summary>
/// Represents information about a character hit within a glyph run.
/// </summary>
/// <remarks>
/// The CharacterHit structure provides information about the index of the first
/// character that got hit as well as information about leading or trailing edge.
/// </remarks>
public readonly struct CharacterHit : IEquatable<CharacterHit>
{
/// <summary>
/// Initializes a new instance of the <see cref="CharacterHit"/> structure.
/// </summary>
/// <param name="firstCharacterIndex">Index of the first character that got hit.</param>
/// <param name="trailingLength">In the case of a leading edge, this value is 0. In the case of a trailing edge,
/// this value is the number of code points until the next valid caret position.</param>
public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
{
FirstCharacterIndex = firstCharacterIndex;
TrailingLength = trailingLength;
}
/// <summary>
/// Gets the index of the first character that got hit.
/// </summary>
public int FirstCharacterIndex { get; }
/// <summary>
/// Gets the trailing length value for the character that got hit.
/// </summary>
public int TrailingLength { get; }
public bool Equals(CharacterHit other)
{
return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength;
}
public override bool Equals(object obj)
{
return obj is CharacterHit other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return FirstCharacterIndex * 397 ^ TrailingLength;
}
}
public static bool operator ==(CharacterHit left, CharacterHit right)
{
return left.Equals(right);
}
public static bool operator !=(CharacterHit left, CharacterHit right)
{
return !left.Equals(right);
}
}
}

16
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -187,6 +187,22 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Draws a glyph run.
/// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
Contract.Requires<ArgumentNullException>(glyphRun != null);
if (foreground != null)
{
PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin);
}
}
/// <summary>
/// Draws a filled rectangle.
/// </summary>

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

@ -2,17 +2,17 @@
// 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.Linq;
using Avalonia.Media.Fonts;
namespace Avalonia.Media
{
public sealed class FontFamily
{
public const string DefaultFontFamilyName = "$Default";
static FontFamily()
{
Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
Default = new FontFamily(DefaultFontFamilyName);
}
/// <inheritdoc />
@ -57,15 +57,6 @@ namespace Avalonia.Media
/// </summary>
public static FontFamily Default { get; }
/// <summary>
/// Represents all font families in the system. This can be an expensive call depending on platform implementation.
/// </summary>
/// <remarks>
/// Consider using the new <see cref="FontManager"/> instead.
/// </remarks>
public static IEnumerable<FontFamily> SystemFontFamilies =>
FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
/// <summary>
/// Gets the primary family name of the font family.
/// </summary>
@ -86,10 +77,16 @@ namespace Avalonia.Media
/// Gets the key for associated assets.
/// </summary>
/// <value>
/// The family familyNames.
/// The family key.
/// </value>
/// <remarks>Key is only used for custom fonts.</remarks>
public FontFamilyKey Key { get; }
/// <summary>
/// Returns <c>True</c> if this instance is the system's default.
/// </summary>
public bool IsDefault => Name.Equals(DefaultFontFamilyName);
/// <summary>
/// Implicit conversion of string to FontFamily
/// </summary>
@ -188,6 +185,21 @@ namespace Avalonia.Media
}
}
public static bool operator !=(FontFamily a, FontFamily b)
{
return !(a == b);
}
public static bool operator ==(FontFamily a, FontFamily b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))

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

@ -1,8 +1,10 @@
// 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.Fonts;
using Avalonia.Platform;
namespace Avalonia.Media
@ -11,9 +13,53 @@ namespace Avalonia.Media
/// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts.
/// It is also responsible for the font fallback.
/// </summary>
public abstract class FontManager
public sealed class FontManager
{
public static readonly FontManager Default = CreateDefault();
private readonly ConcurrentDictionary<FontKey, Typeface> _typefaceCache =
new ConcurrentDictionary<FontKey, Typeface>();
private readonly FontFamily _defaultFontFamily;
private FontManager(IFontManagerImpl platformImpl)
{
PlatformImpl = platformImpl;
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
_defaultFontFamily = new FontFamily(DefaultFontFamilyName);
}
public static FontManager Current
{
get
{
var current = AvaloniaLocator.Current.GetService<FontManager>();
if (current != null)
{
return current;
}
var renderInterface = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var fontManagerImpl = renderInterface?.CreateFontManager();
if (fontManagerImpl == null)
{
return null;
}
current = new FontManager(fontManagerImpl);
AvaloniaLocator.CurrentMutable.Bind<FontManager>().ToConstant(current);
return current;
}
}
/// <summary>
///
/// </summary>
public IFontManagerImpl PlatformImpl { get; }
/// <summary>
/// Gets the system's default font family's name.
@ -21,25 +67,55 @@ namespace Avalonia.Media
public string DefaultFontFamilyName
{
get;
protected set;
}
/// <summary>
/// Get all installed fonts in the system.
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
/// Get all installed font family names.
/// </summary>
public abstract IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
/// <summary>
/// Get a cached typeface from specified parameters.
/// Returns a new typeface, or an existing one if a matching typeface exists.
/// </summary>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="fontStyle">The font style.</param>
/// <returns>
/// The cached typeface.
/// The typeface.
/// </returns>
public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal)
{
while (true)
{
if (fontFamily.IsDefault)
{
fontFamily = _defaultFontFamily;
}
var key = new FontKey(fontFamily, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface))
{
return typeface;
}
typeface = new Typeface(fontFamily, fontWeight, fontStyle);
if (_typefaceCache.TryAdd(key, typeface))
{
return typeface;
}
if (fontFamily == _defaultFontFamily)
{
return null;
}
fontFamily = _defaultFontFamily;
}
}
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
@ -53,60 +129,13 @@ namespace Avalonia.Media
/// <returns>
/// The matched typeface.
/// </returns>
public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
public static FontManager CreateDefault()
{
var platformImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
if (platformImpl != null)
{
return new PlatformFontManager(platformImpl);
}
return new EmptyFontManager();
}
private class PlatformFontManager : FontManager
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal,
FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null)
{
private readonly IFontManagerImpl _platformImpl;
public PlatformFontManager(IFontManagerImpl platformImpl)
{
_platformImpl = platformImpl;
DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
_platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
public override Typeface GetCachedTypeface(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
{
public EmptyFontManager()
{
DefaultFontFamilyName = "Empty";
}
public override IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
new[] { DefaultFontFamilyName };
public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle);
public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null) => null;
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
null;
}
}
}

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

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
@ -54,25 +55,19 @@ namespace Avalonia.Media.Fonts
/// </value>
internal IReadOnlyList<string> Names { get; }
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through the collection.
/// Returns an enumerator for the name collection.
/// </summary>
/// <returns>
/// An enumerator that can be used to iterate through the collection.
/// </returns>
public IEnumerator<string> GetEnumerator()
public ImmutableReadOnlyListStructEnumerator<string> GetEnumerator()
{
return Names.GetEnumerator();
return new ImmutableReadOnlyListStructEnumerator<string>(this);
}
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc />
/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
/// </returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
@ -131,6 +126,21 @@ namespace Avalonia.Media.Fonts
}
}
public static bool operator !=(FamilyNameCollection a, FamilyNameCollection b)
{
return !(a == b);
}
public static bool operator ==(FamilyNameCollection a, FamilyNameCollection b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
/// <summary>
/// Determines whether the specified <see cref="object" />, is equal to this instance.
/// </summary>

15
src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs

@ -58,6 +58,21 @@ namespace Avalonia.Media.Fonts
}
}
public static bool operator !=(FontFamilyKey a, FontFamilyKey b)
{
return !(a == b);
}
public static bool operator ==(FontFamilyKey a, FontFamilyKey b)
{
if (ReferenceEquals(a, b))
{
return true;
}
return !(a is null) && a.Equals(b);
}
/// <summary>
/// Determines whether the specified <see cref="object" />, is equal to this instance.
/// </summary>

15
src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs

@ -10,13 +10,6 @@ namespace Avalonia.Media.Fonts
{
public static class FontFamilyLoader
{
private static readonly IAssetLoader s_assetLoader;
static FontFamilyLoader()
{
s_assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
}
/// <summary>
/// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
/// </summary>
@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
{
var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var matchingAssets =
availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf"));
@ -58,9 +53,11 @@ namespace Avalonia.Media.Fonts
/// <returns></returns>
private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
{
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location);
var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
string compareTo;

15
src/Skia/Avalonia.Skia/FontKey.cs → src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@ -2,24 +2,26 @@
// 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
namespace Avalonia.Media.Fonts
{
internal readonly struct FontKey : IEquatable<FontKey>
public readonly struct FontKey : IEquatable<FontKey>
{
public readonly FontFamily FontFamily;
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontWeight weight, FontStyle style)
public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
{
FontFamily = fontFamily;
Style = style;
Weight = weight;
}
public override int GetHashCode()
{
var hash = 17;
var hash = FontFamily.GetHashCode();
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
@ -33,7 +35,8 @@ namespace Avalonia.Skia
public bool Equals(FontKey other)
{
return Style == other.Style &&
return FontFamily == other.FontFamily &&
Style == other.Style &&
Weight == other.Weight;
}
}

457
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -0,0 +1,457 @@
// 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 Avalonia.Platform;
using Avalonia.Utility;
namespace Avalonia.Media
{
/// <summary>
/// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style.
/// </summary>
public sealed class GlyphRun : IDisposable
{
private static readonly IPlatformRenderInterface s_platformRenderInterface =
AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
private IGlyphRunImpl _glyphRunImpl;
private GlyphTypeface _glyphTypeface;
private double _fontRenderingEmSize;
private Rect? _bounds;
private ReadOnlySlice<ushort> _glyphIndices;
private ReadOnlySlice<double> _glyphAdvances;
private ReadOnlySlice<Vector> _glyphOffsets;
private ReadOnlySlice<ushort> _glyphClusters;
private ReadOnlySlice<char> _characters;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class.
/// </summary>
public GlyphRun()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <param name="characters">The characters.</param>
/// <param name="glyphClusters">The glyph clusters.</param>
/// <param name="bidiLevel">The bidi level.</param>
/// <param name="bounds">The bound.</param>
public GlyphRun(
GlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlySlice<ushort> glyphIndices,
ReadOnlySlice<double> glyphAdvances = default,
ReadOnlySlice<Vector> glyphOffsets = default,
ReadOnlySlice<char> characters = default,
ReadOnlySlice<ushort> glyphClusters = default,
int bidiLevel = 0,
Rect? bounds = null)
{
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
GlyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets;
Characters = characters;
GlyphClusters = glyphClusters;
BidiLevel = bidiLevel;
Initialize(bounds);
}
/// <summary>
/// Gets or sets the <see cref="Media.GlyphTypeface"/> for the <see cref="GlyphRun"/>.
/// </summary>
public GlyphTypeface GlyphTypeface
{
get => _glyphTypeface;
set => Set(ref _glyphTypeface, value);
}
/// <summary>
/// Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
/// </summary>
public double FontRenderingEmSize
{
get => _fontRenderingEmSize;
set => Set(ref _fontRenderingEmSize, value);
}
/// <summary>
/// Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
/// </summary>
public ReadOnlySlice<ushort> GlyphIndices
{
get => _glyphIndices;
set => Set(ref _glyphIndices, value);
}
/// <summary>
/// Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
/// </summary>
public ReadOnlySlice<double> GlyphAdvances
{
get => _glyphAdvances;
set => Set(ref _glyphAdvances, value);
}
/// <summary>
/// Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<Vector> GlyphOffsets
{
get => _glyphOffsets;
set => Set(ref _glyphOffsets, value);
}
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
}
/// <summary>
/// Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index.
/// </summary>
public ReadOnlySlice<ushort> GlyphClusters
{
get => _glyphClusters;
set => Set(ref _glyphClusters, value);
}
/// <summary>
/// Gets or sets the bidirectional nesting level of the <see cref="GlyphRun"/>.
/// </summary>
public int BidiLevel
{
get;
set;
}
/// <summary>
///
/// </summary>
internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
/// <summary>
///
/// </summary>
internal bool IsLeftToRight => ((BidiLevel & 1) == 0);
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Rect Bounds
{
get
{
if (_bounds == null)
{
_bounds = CalculateBounds();
}
return _bounds.Value;
}
set => _bounds = value;
}
public IGlyphRunImpl GlyphRunImpl
{
get
{
if (_glyphRunImpl == null)
{
Initialize(null);
}
return _glyphRunImpl;
}
}
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var distance = 0.0;
var end = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
for (var i = 0; i < _glyphClusters.Length; i++)
{
if (_glyphClusters[i] >= end)
{
break;
}
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[i];
distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
distance += GlyphAdvances[i];
}
}
return distance;
}
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
// Before
if (distance < 0)
{
isInside = false;
var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
}
//After
if (distance > Bounds.Size.Width)
{
isInside = false;
var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
}
//Within
var currentX = 0.0;
var index = 0;
for (; index < GlyphIndices.Length; index++)
{
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[index];
currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
currentX += GlyphAdvances[index];
}
if (currentX > distance)
{
break;
}
}
if (index == GlyphIndices.Length)
{
index--;
}
var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
isInside = distance < currentX && width > 0;
var isTrailing = distance > currentX - width / 2;
return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
}
public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
if (characterHit.TrailingLength == 0)
{
return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
}
var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
}
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
return characterHit.TrailingLength == 0 ?
FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) :
new CharacterHit(characterHit.FirstCharacterIndex);
}
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T x, T y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
private static readonly IComparer<ushort> s_ascendingComparer = Comparer<ushort>.Default;
private static readonly IComparer<ushort> s_descendingComparer = new ReverseComparer<ushort>();
internal CharacterHit FindNearestCharacterHit(int index, out double width)
{
width = 0.0;
if (index < 0)
{
return default;
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
var clusters = _glyphClusters.AsSpan();
int start;
if (index == 0 && clusters[0] == 0)
{
start = 0;
}
else
{
// Find the start of the cluster at the character index.
start = clusters.BinarySearch((ushort)index, comparer);
}
// No cluster found.
if (start < 0)
{
while (index > 0 && start < 0)
{
index--;
start = clusters.BinarySearch((ushort)index, comparer);
}
if (start < 0)
{
return default;
}
}
var trailingLength = 0;
var currentCluster = clusters[start];
while (start > 0 && clusters[start - 1] == currentCluster)
{
start--;
}
for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex)
{
if (_glyphClusters[lastIndex] != currentCluster)
{
break;
}
if (GlyphAdvances.IsEmpty)
{
var glyph = GlyphIndices[lastIndex];
width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
else
{
width += GlyphAdvances[lastIndex];
}
trailingLength++;
}
return new CharacterHit(currentCluster, trailingLength);
}
private Rect CalculateBounds()
{
var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale;
var width = 0.0;
if (GlyphAdvances.IsEmpty)
{
foreach (var glyph in GlyphIndices)
{
width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
}
else
{
foreach (var advance in GlyphAdvances)
{
width += advance;
}
}
return new Rect(0, 0, width, height);
}
private void Set<T>(ref T field, T value)
{
if (_glyphRunImpl != null)
{
throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'");
}
field = value;
}
private void Initialize(Rect? bounds)
{
if (GlyphIndices.Length == 0)
{
throw new InvalidOperationException();
}
var glyphCount = GlyphIndices.Length;
if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount)
{
throw new InvalidOperationException();
}
if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount)
{
throw new InvalidOperationException();
}
_glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width);
if (bounds.HasValue)
{
_bounds = bounds;
}
else
{
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
_bounds = new Rect(0, 0, width, height);
}
}
void IDisposable.Dispose()
{
_glyphRunImpl?.Dispose();
}
}
}

50
src/Avalonia.Visuals/Media/GlyphRunDrawing.cs

@ -0,0 +1,50 @@
// 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.
namespace Avalonia.Media
{
public class GlyphRunDrawing : Drawing
{
public static readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<GlyphRunDrawing, IBrush>(nameof(Foreground));
public static readonly StyledProperty<GlyphRun> GlyphRunProperty =
AvaloniaProperty.Register<GlyphRunDrawing, GlyphRun>(nameof(GlyphRun));
public static readonly StyledProperty<Point> BaselineOriginProperty =
AvaloniaProperty.Register<GlyphRunDrawing, Point>(nameof(BaselineOrigin));
public IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public GlyphRun GlyphRun
{
get => GetValue(GlyphRunProperty);
set => SetValue(GlyphRunProperty, value);
}
public Point BaselineOrigin
{
get => GetValue(BaselineOriginProperty);
set => SetValue(BaselineOriginProperty, value);
}
public override void Draw(DrawingContext context)
{
if (GlyphRun == null)
{
return;
}
context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
}
public override Rect GetBounds()
{
return GlyphRun?.Bounds ?? default;
}
}
}

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

@ -9,11 +9,9 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
private static readonly IPlatformRenderInterface s_platformRenderInterface =
AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface))
public GlyphTypeface(Typeface typeface)
{
PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface);
}
public GlyphTypeface(IGlyphTypefaceImpl platformImpl)
@ -68,6 +66,11 @@ namespace Avalonia.Media
/// </summary>
public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
/// <summary>
/// A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement.
/// </summary>
public bool IsFixedPitch => PlatformImpl.IsFixedPitch;
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>

19
src/Avalonia.Visuals/Media/Transform.cs

@ -28,6 +28,16 @@ namespace Avalonia.Media
/// </summary>
public abstract Matrix Value { get; }
/// <summary>
/// Parses a <see cref="Transform"/> string.
/// </summary>
/// <param name="s">Six comma-delimited double values that describe the new <see cref="Transform"/>. For details check <see cref="Matrix.Parse(string)"/> </param>
/// <returns>The <see cref="Transform"/>.</returns>
public static Transform Parse(string s)
{
return new MatrixTransform(Matrix.Parse(s));
}
/// <summary>
/// Raises the <see cref="Changed"/> event.
/// </summary>
@ -35,5 +45,14 @@ namespace Avalonia.Media
{
Changed?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Returns a String representing this transform matrix instance.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString()
{
return Value.ToString();
}
}
}

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

@ -13,8 +13,6 @@ namespace Avalonia.Media
[DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
public class Typeface : IEquatable<Typeface>
{
public static readonly Typeface Default = new Typeface(FontFamily.Default);
private GlyphTypeface _glyphTypeface;
/// <summary>
@ -50,6 +48,8 @@ namespace Avalonia.Media
{
}
public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default);
/// <summary>
/// Gets the font family.
/// </summary>

8
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -86,6 +86,14 @@ namespace Avalonia.Platform
/// <param name="text">The text.</param>
void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
/// <summary>
/// Draws a glyph run.
/// </summary>
/// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin);
/// <summary>
/// Creates a new <see cref="IRenderTargetBitmapImpl"/> that can be used as a render layer
/// for the current render target.

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

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
namespace Avalonia.Platform
{
@ -12,7 +13,7 @@ namespace Avalonia.Platform
/// <summary>
/// Gets the system's default font family's name.
/// </summary>
string DefaultFontFamilyName { get; }
string GetDefaultFontFamilyName();
/// <summary>
/// Get all installed fonts in the system.
@ -20,17 +21,6 @@ namespace Avalonia.Platform
/// </summary>
IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
/// <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>
@ -39,10 +29,20 @@ namespace Avalonia.Platform
/// <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>
/// <param name="fontKey">The matching font key.</param>
/// <returns>
/// The typeface.
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey);
/// <summary>
/// Creates a glyph typeface.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>0
/// The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
/// </returns>
Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null);
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
}
}

12
src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs

@ -0,0 +1,12 @@
// 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
{
/// <summary>
/// Actual implementation of a glyph run that stores platform dependent resources.
/// </summary>
public interface IGlyphRunImpl : IDisposable { }
}

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

@ -47,6 +47,11 @@ namespace Avalonia.Platform
/// </summary>
int StrikethroughThickness { get; }
/// <summary>
/// A <see cref="bool"/> value indicating whether all glyphs in the font have the same advancement.
/// </summary>
bool IsFixedPitch { get; }
/// <summary>
/// Returns an glyph index for the specified codepoint.
/// </summary>

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

@ -113,12 +113,17 @@ namespace Avalonia.Platform
IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
/// <summary>
/// Creates a glyph typeface for specified typeface.
/// Creates a font manager implementation.
/// </summary>
/// <param name="typeface">The typeface.</param>
/// <returns>
/// The glyph typeface implementation.
/// </returns>
IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
/// <returns>The font manager.</returns>
IFontManagerImpl CreateFontManager();
/// <summary>
/// Creates a platform implementation of a glyph run.
/// </summary>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="width">The glyph run's width.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
}
}

2
src/Avalonia.Visuals/Point.cs

@ -175,7 +175,7 @@ namespace Avalonia
/// Parses a <see cref="Point"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The <see cref="Thickness"/>.</returns>
/// <returns>The <see cref="Point"/>.</returns>
public static Point Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point."))

11
src/Avalonia.Visuals/RelativePoint.cs

@ -177,5 +177,16 @@ namespace Avalonia
unit);
}
}
/// <summary>
/// Returns a String representing this RelativePoint instance.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString()
{
return _unit == RelativeUnit.Absolute ?
_point.ToString() :
string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100);
}
}
}

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

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

15
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -190,6 +190,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
/// <inheritdoc/>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
{
Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground)));
}
else
{
++_drawOperationindex;
}
}
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");

91
src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.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.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a glyph run draw.
/// </summary>
internal class GlyphRunNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRunNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run to draw.</param>
/// <param name="baselineOrigin">The baseline origin of the glyph run.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public GlyphRunNode(
Matrix transform,
IBrush foreground,
GlyphRun glyphRun,
Point baselineOrigin,
IDictionary<IVisual, Scene> childScenes = null)
: base(glyphRun.Bounds, transform, null)
{
Transform = transform;
Foreground = foreground?.ToImmutable();
GlyphRun = glyphRun;
BaselineOrigin = baselineOrigin;
ChildScenes = childScenes;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the foreground brush.
/// </summary>
public IBrush Foreground { get; }
/// <summary>
/// Gets the glyph run to draw.
/// </summary>
public GlyphRun GlyphRun { get; }
/// <summary>
/// Gets the baseline origin.
/// </summary>
public Point BaselineOrigin { get; set; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
}
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="foreground">The foreground of the other draw operation.</param>
/// <param name="glyphRun">The glyph run of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun)
{
return transform == Transform &&
Equals(foreground, Foreground) &&
Equals(glyphRun, GlyphRun);
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.Contains(p);
}
}

154
src/Avalonia.Visuals/Utility/ReadOnlySlice.cs

@ -0,0 +1,154 @@
// 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;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Utility
{
/// <summary>
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
/// </summary>
/// <typeparam name="T">The type of elements in the slice.</typeparam>
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T>
{
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length)
{
Buffer = buffer;
Start = start;
Length = length;
}
/// <summary>
/// Gets the start.
/// </summary>
/// <value>
/// The start.
/// </value>
public int Start { get; }
/// <summary>
/// Gets the end.
/// </summary>
/// <value>
/// The end.
/// </value>
public int End => Start + Length - 1;
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public int Length { get; }
/// <summary>
/// Gets a value that indicates whether this instance of <see cref="ReadOnlySpan{T}"/> is Empty.
/// </summary>
public bool IsEmpty => Length == 0;
/// <summary>
/// The buffer.
/// </summary>
public ReadOnlyMemory<T> Buffer { get; }
public T this[int index] => Buffer.Span[Start + index];
/// <summary>
/// Returns a span of the underlying buffer.
/// </summary>
/// <returns>The <see cref="ReadOnlySpan{T}"/> of the underlying buffer.</returns>
public ReadOnlySpan<T> AsSpan()
{
return Buffer.Span.Slice(Start, Length);
}
/// <summary>
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
/// </summary>
/// <param name="start">The start of the sub slice.</param>
/// <param name="length">The length of the sub slice.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
public ReadOnlySlice<T> AsSlice(int start, int length)
{
if (start < 0 || start >= Length)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (Start + start > End)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(Buffer, Start + start, length);
}
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
public ReadOnlySlice<T> Take(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(Buffer, Start, length);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
public ReadOnlySlice<T> Skip(int length)
{
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(Buffer, Start + length, Length - length);
}
/// <summary>
/// Returns an enumerator for the slice.
/// </summary>
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<T>(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
int IReadOnlyCollection<T>.Count => Length;
T IReadOnlyList<T>.this[int index] => this[index];
public static implicit operator ReadOnlySlice<T>(T[] array)
{
return new ReadOnlySlice<T>(array);
}
public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory)
{
return new ReadOnlySlice<T>(memory);
}
}
}

21
src/Avalonia.Visuals/Vector.cs

@ -4,6 +4,7 @@
using System;
using System.Globalization;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
using JetBrains.Annotations;
namespace Avalonia
@ -85,6 +86,22 @@ namespace Avalonia
public static Vector operator /(Vector vector, double scale)
=> Divide(vector, scale);
/// <summary>
/// Parses a <see cref="Vector"/> string.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The <see cref="Vector"/>.</returns>
public static Vector Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
{
return new Vector(
tokenizer.ReadDouble(),
tokenizer.ReadDouble()
);
}
}
/// <summary>
/// Length of the vector
/// </summary>
@ -166,9 +183,9 @@ namespace Avalonia
}
/// <summary>
/// Returns the string representation of the point.
/// Returns the string representation of the vector.
/// </summary>
/// <returns>The string representation of the point.</returns>
/// <returns>The string representation of the vector.</returns>
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs

@ -25,7 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if ((tt?.Values.FirstOrDefault() is XamlIlTypeExtensionNode tn))
{
targetType = tn.Type;
targetType = tn.Value;
}
else
{

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github

@ -1 +1 @@
Subproject commit ad9915e19398a49c5a11b66000c361659ca692b3
Subproject commit 4c4b6cf8ff0894c925d87b27d4fc7a064440c218

14
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -232,6 +232,20 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
(float)baselineOrigin.Y, glyphRunImpl.Paint);
}
}
/// <inheritdoc />
public IRenderTargetBitmapImpl CreateLayer(Size size)
{

77
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -1,9 +1,11 @@
// 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.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
@ -13,13 +15,11 @@ namespace Avalonia.Skia
{
private SKFontManager _skFontManager = SKFontManager.Default;
public FontManagerImpl()
public string GetDefaultFontFamilyName()
{
DefaultFontFamilyName = SKTypeface.Default.FamilyName;
return SKTypeface.Default.FamilyName;
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
@ -30,53 +30,86 @@ namespace Avalonia.Skia
return _skFontManager.FontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
}
[ThreadStatic] private static string[] t_languageTagBuffer;
public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
FontFamily fontFamily = null, CultureInfo culture = null)
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
var fontFamilyName = FontFamily.Default.Name;
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
if (t_languageTagBuffer == null)
{
t_languageTagBuffer = new string[2];
}
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
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);
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null)
{
continue;
}
fontFamilyName = familyName;
fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
break;
return true;
}
}
else
{
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
(SKFontStyleSlant)fontStyle,
new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null)
{
fontFamilyName = skTypeface.FamilyName;
fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
return true;
}
}
fontKey = default;
return false;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
var skTypeface = SKTypeface.Default;
if (typeface.FontFamily.Key == null)
{
foreach (var familyName in typeface.FontFamily.FamilyNames)
{
skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
if (skTypeface == SKTypeface.Default)
{
continue;
}
break;
}
}
else
{
var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
skTypeface = fontCollection.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
}
return GetTypeface(fontFamilyName, fontWeight, fontStyle);
return new GlyphTypefaceImpl(skTypeface);
}
}
}

4
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -29,7 +29,7 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl;
_paint = new SKPaint
{
@ -38,7 +38,7 @@ namespace Avalonia.Skia
IsAntialias = true,
LcdRenderText = true,
SubpixelText = true,
Typeface = entry.SKTypeface,
Typeface = glyphTypeface.Typeface,
TextSize = (float)fontSize,
TextAlign = textAlignment.ToSKTextAlign()
};

35
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -0,0 +1,35 @@
// 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;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <inheritdoc />
public class GlyphRunImpl : IGlyphRunImpl
{
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
{
Paint = paint;
TextBlob = textBlob;
}
/// <summary>
/// Gets the paint to draw with.
/// </summary>
public SKPaint Paint { get; }
/// <summary>
/// Gets the text blob to draw.
/// </summary>
public SKTextBlob TextBlob { get; }
void IDisposable.Dispose()
{
TextBlob.Dispose();
Paint.Dispose();
}
}
}

10
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -14,9 +14,9 @@ namespace Avalonia.Skia
{
private bool _isDisposed;
public GlyphTypefaceImpl(Typeface typeface)
public GlyphTypefaceImpl(SKTypeface typeface)
{
Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
Typeface = typeface;
Face = new Face(GetTable)
{
@ -61,6 +61,8 @@ namespace Avalonia.Skia
{
StrikethroughThickness = strikethroughThickness;
}
IsFixedPitch = Typeface.IsFixedPitch;
}
public Face Face { get; }
@ -81,7 +83,6 @@ namespace Avalonia.Skia
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int LineGap { get; }
//ToDo: Get these values from HarfBuzz
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int UnderlinePosition { get; }
@ -94,6 +95,9 @@ namespace Avalonia.Skia
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public bool IsFixedPitch { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{

96
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls.Platform.Surfaces;
@ -18,9 +17,6 @@ namespace Avalonia.Skia
/// </summary>
internal class PlatformRenderInterface : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; }
@ -60,7 +56,7 @@ namespace Avalonia.Skia
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{
return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans);
}
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
@ -155,9 +151,95 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format);
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
/// <inheritdoc />
public IFontManagerImpl CreateFontManager()
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
return new FontManagerImpl();
}
/// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
var count = glyphRun.GlyphIndices.Length;
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
var typeface = glyphTypeface.Typeface;
var paint = new SKPaint
{
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
if (glyphRun.GlyphOffsets.IsEmpty)
{
width = 0;
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
if (!glyphTypeface.IsFixedPitch)
{
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++)
{
positions[i] = (float)width;
if (glyphRun.GlyphAdvances.IsEmpty)
{
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
width += glyphRun.GlyphAdvances[i];
}
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
}
else
{
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
width = currentX;
}
var textBlob = textBlobBuilder.Build();
return new GlyphRunImpl(paint, textBlob);
}
}
}
}

4
src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")]

47
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -5,58 +5,59 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> _fontFamilies =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
private readonly ConcurrentDictionary<FontKey, SKTypeface> _typefaces =
new ConcurrentDictionary<FontKey, SKTypeface>();
public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry)
public void AddTypeface(FontKey key, SKTypeface typeface)
{
if (!_fontFamilies.TryGetValue(familyName, out var fontFamily))
{
fontFamily = new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>();
_fontFamilies.TryAdd(familyName, fontFamily);
}
fontFamily.TryAdd(key, entry);
_typefaces.TryAdd(key, typeface);
}
public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle)
public SKTypeface Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
var key = new FontKey(fontWeight, fontStyle);
var key = new FontKey(fontFamily, fontWeight, fontStyle);
return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default);
return GetNearestMatch(_typefaces, key);
}
private static TypefaceCollectionEntry GetFallback(IDictionary<FontKey, TypefaceCollectionEntry> fontFamily, FontKey key)
private static SKTypeface GetNearestMatch(IDictionary<FontKey, SKTypeface> typefaces, FontKey key)
{
var keys = fontFamily.Keys.Where(
if (typefaces.ContainsKey(key))
{
return typefaces[key];
}
var keys = typefaces.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray();
if (!keys.Any())
{
keys = fontFamily.Keys.Where(
keys = typefaces.Keys.Where(
x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
if (!keys.Any())
{
keys = fontFamily.Keys.Where(
keys = typefaces.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
(x.Style >= key.Style || x.Style < key.Style)).ToArray();
}
}
key = keys.FirstOrDefault();
if (keys.Length == 0)
{
return SKTypeface.Default;
}
fontFamily.TryGetValue(key, out var entry);
key = keys[0];
return entry;
return typefaces[key];
}
}
}

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

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

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

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

47
src/Skia/Avalonia.Skia/TypefaceCache.cs

@ -1,47 +0,0 @@
// 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 Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache for Skia typefaces.
/// </summary>
internal static class TypefaceCache
{
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>> s_cache =
new ConcurrentDictionary<string, ConcurrentDictionary<FontKey, TypefaceCollectionEntry>>();
public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
if (fontFamily.Key != null)
{
return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
.Get(fontFamily.Name, fontWeight, fontStyle);
}
var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary<FontKey, TypefaceCollectionEntry>());
var key = new FontKey(fontWeight, fontStyle);
if (typefaceCollection.TryGetValue(key, out var entry))
{
return entry;
}
var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
entry = new TypefaceCollectionEntry(typeface, skTypeface);
typefaceCollection[key] = entry;
return entry;
}
}
}

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

@ -1,19 +0,0 @@
// 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; }
}
}

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

@ -11,6 +11,9 @@ using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment;
namespace Avalonia
{
@ -28,8 +31,6 @@ namespace Avalonia.Direct2D1
{
public class Direct2D1Platform : IPlatformRenderInterface
{
private readonly ConcurrentDictionary<Typeface, GlyphTypefaceImpl> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypefaceImpl>();
private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; }
@ -109,7 +110,6 @@ namespace Avalonia.Direct2D1
{
InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(s_instance);
AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
@ -194,9 +194,57 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, data, size, dpi, stride);
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
/// <inheritdoc />
public IFontManagerImpl CreateFontManager()
{
return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
return new FontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
var glyphCount = glyphRun.GlyphIndices.Length;
var run = new SharpDX.DirectWrite.GlyphRun
{
FontFace = glyphTypeface.FontFace,
FontSize = (float)glyphRun.FontRenderingEmSize
};
var indices = new short[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
indices[i] = (short)glyphRun.GlyphIndices[i];
}
run.Indices = indices;
run.Advances = new float[glyphCount];
width = 0;
for (var i = 0; i < glyphCount; i++)
{
run.Advances[i] = (float)glyphRun.GlyphAdvances[i];
width += run.Advances[i];
}
run.Offsets = new GlyphOffset[glyphCount];
for (var i = 0; i < glyphCount; i++)
{
var offset = glyphRun.GlyphOffsets[i];
run.Offsets[i] = new GlyphOffset
{
AdvanceOffset = (float)offset.X,
AscenderOffset = (float)offset.Y
};
}
return new GlyphRunImpl(run);
}
}
}

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

@ -24,10 +24,11 @@ namespace Avalonia.Direct2D1.Media
{
var fontFamily = typeface.FontFamily;
var fontCollection = GetOrAddFontCollection(fontFamily);
int index;
foreach (var familyName in fontFamily.FamilyNames)
foreach (var name in fontFamily.FamilyNames)
{
if (fontCollection.FindFamilyName(familyName, out var index))
if (fontCollection.FindFamilyName(name, out index))
{
return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
@ -36,9 +37,9 @@ namespace Avalonia.Direct2D1.Media
}
}
InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
InstalledFontCollection.FindFamilyName("Segoe UI", out index);
return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
FontStretch.Normal,
(FontStyle)typeface.Style);

16
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -316,6 +316,22 @@ namespace Avalonia.Direct2D1.Media
}
}
/// <summary>
/// Draws a glyph run.
/// </summary>
/// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param>
/// <param name="baselineOrigin"></param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{
using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
_renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural);
}
}
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
if (_layerFactory != null)

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

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
@ -14,14 +15,12 @@ namespace Avalonia.Direct2D1.Media
{
internal class FontManagerImpl : IFontManagerImpl
{
public FontManagerImpl()
public string GetDefaultFontFamilyName()
{
//ToDo: Implement a real lookup of the system's default font.
DefaultFontFamilyName = "segoe ui";
return "Segoe UI";
}
public string DefaultFontFamilyName { get; }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -36,17 +35,9 @@ namespace Avalonia.Direct2D1.Media
return fontFamilies;
}
public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
//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++)
@ -60,12 +51,21 @@ namespace Avalonia.Direct2D1.Media
continue;
}
fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
break;
fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle);
return true;
}
return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
fontKey = default;
return false;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return new GlyphTypefaceImpl(typeface);
}
}
}

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

@ -22,10 +22,16 @@ namespace Avalonia.Direct2D1.Media
{
Text = text;
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))
var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont;
var familyName = font.FontFamily.FamilyNames.GetString(0);
using (var textFormat = new DWrite.TextFormat(
Direct2D1Platform.DirectWriteFactory,
familyName,
font.FontFamily.FontCollection,
(DWrite.FontWeight)typeface.Weight,
(DWrite.FontStyle)typeface.Style,
DWrite.FontStretch.Normal,
(float)fontSize))
{
textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;

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

@ -0,0 +1,19 @@
using Avalonia.Platform;
namespace Avalonia.Direct2D1.Media
{
internal class GlyphRunImpl : IGlyphRunImpl
{
public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun)
{
GlyphRun = glyphRun;
}
public SharpDX.DirectWrite.GlyphRun GlyphRun { get; }
public void Dispose()
{
GlyphRun?.Dispose();
}
}
}

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

@ -17,7 +17,7 @@ namespace Avalonia.Direct2D1.Media
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
FontFace = new FontFace(DWFont);
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>();
Face = new Face(GetTable);
@ -59,6 +59,8 @@ namespace Avalonia.Direct2D1.Media
{
StrikethroughThickness = strikethroughThickness;
}
IsFixedPitch = FontFace.IsMonospacedFont;
}
private Blob GetTable(Face face, Tag tag)
@ -82,7 +84,7 @@ namespace Avalonia.Direct2D1.Media
public SharpDX.DirectWrite.Font DWFont { get; }
public FontFace FontFace { get; }
public FontFace1 FontFace { get; }
public Face Face { get; }
@ -113,6 +115,9 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public int StrikethroughThickness { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public bool IsFixedPitch { get; }
/// <inheritdoc cref="IGlyphTypefaceImpl"/>
public ushort GetGlyph(uint codepoint)
{

4
src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs

@ -2,9 +2,13 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Platform;
using Avalonia.Direct2D1;
[assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize),
typeof(Direct2DChecker))]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")]

44
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -4,6 +4,7 @@
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
@ -245,6 +246,23 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(false, item.IsSelected);
}
[Fact]
public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection()
{
var items = new AvaloniaList<string>(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
};
Prepare(target);
target.Scroll.Offset = new Vector(0, 1);
items.RemoveRange(0, 11);
}
private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
{
_mouse.Click(listBox, item, mouseButton);
@ -383,14 +401,26 @@ namespace Avalonia.Controls.UnitTests
private FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
new ScrollContentPresenter
new Panel
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
}.RegisterInNameScope(scope));
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
}.RegisterInNameScope(scope),
new ScrollBar
{
Name = "verticalScrollBar",
[~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty],
[~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty],
}
}
});
}
private void Prepare(ListBox target)

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

@ -0,0 +1,111 @@
using System;
using System.Reflection;
using Avalonia.Direct2D1.Media;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Direct2D1.UnitTests.Media
{
public class FontManagerImplTests
{
private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono";
[Fact]
public void Should_Create_Typeface_From_Fallback()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var defaultName = fontManager.GetDefaultFontFamilyName();
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);
}
}
[Fact]
public void Should_Create_Typeface_For_Unknown_Font()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var font = glyphTypeface.DWFont;
var defaultName = fontManager.GetDefaultFontFamilyName();
Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0));
Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
}
}
[Fact]
public void Should_Load_Typeface_From_Resource()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri)));
var font = glyphTypeface.DWFont;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
}
}
[Fact]
public void Should_Load_Nearest_Matching_Font()
{
using (AvaloniaLocator.EnterScope())
{
Direct2D1Platform.Initialize();
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic));
var font = glyphTypeface.DWFont;
Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
}
}
}
}

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

@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
.Returns(new FormattedTextMock("TEST"));
renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl());
var streamGeometry = new Mock<IStreamGeometryImpl>();
streamGeometry.Setup(x =>
x.Open())

23
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -307,6 +307,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("child", child.Name);
}
[Fact]
public void ControlTemplate_With_TargetType_Is_Operational()
{
var xaml = @"
<ControlTemplate xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
TargetType='{x:Type ContentControl}'>
<ContentPresenter Content='{TemplateBinding Content}' />
</ControlTemplate>
";
var template = AvaloniaXamlLoader.Parse<ControlTemplate>(xaml);
Assert.Equal(typeof(ContentControl), template.TargetType);
Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control);
}
[Fact]
public void ControlTemplate_With_Panel_Children_Are_Added()
{
@ -704,11 +721,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact(Skip =
@"Doesn't work with Portable.xaml, it's working in different creation order -
Handled in test 'Control_Is_Added_To_Parent_Before_Final_EndInit'
do we need it?")]
[Fact]
public void Control_Is_Added_To_Parent_Before_Properties_Are_Set()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))

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

@ -5,6 +5,7 @@ using System.Xml;
using Avalonia.Controls;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
@ -38,6 +39,57 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void DataTemplate_Can_Be_Added_To_Style_Resources()
{
using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Styles>
<Style>
<Style.Resources>
<DataTemplate x:Key='dataTemplate'><TextBlock/></DataTemplate>
</Style.Resources>
</Style>
</UserControl.Styles>
</UserControl>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var dataTemplate = (DataTemplate)((Style)userControl.Styles[0]).Resources["dataTemplate"];
Assert.NotNull(dataTemplate);
}
}
[Fact]
public void ControlTemplate_Can_Be_Added_To_Style_Resources()
{
using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Styles>
<Style>
<Style.Resources>
<ControlTemplate x:Key='controlTemplate' TargetType='{x:Type Button}'>
<ContentPresenter Content='{TemplateBinding Content}'/>
</ControlTemplate>
</Style.Resources>
</Style>
</UserControl.Styles>
</UserControl>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var controlTemplate = (ControlTemplate)((Style)userControl.Styles[0]).Resources["controlTemplate"];
Assert.NotNull(controlTemplate);
Assert.Equal(typeof(Button), controlTemplate.TargetType);
}
}
[Fact]
public void SolidColorBrush_Can_Be_Added_To_Style_Resources()
{

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

@ -53,7 +53,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text,
new Typeface(fontFamily, fontWeight, fontStyle),
FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle),
fontSize,
textAlignment,
wrapping,

93
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@ -0,0 +1,93 @@
using System;
using System.Reflection;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using SkiaSharp;
using Xunit;
namespace Avalonia.Skia.UnitTests
{
public class FontManagerImplTests
{
private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono";
[Fact]
public void Should_Create_Typeface_From_Fallback()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
}
[Fact]
public void Should_Create_Typeface_For_Unknown_Font()
{
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily("Unknown")));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
}
[Fact]
public void Should_Load_Typeface_From_Resource()
{
using (AvaloniaLocator.EnterScope())
{
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri)));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
}
}
[Fact]
public void Should_Load_Nearest_Matching_Font()
{
using (AvaloniaLocator.EnterScope())
{
var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(assetLoader);
var fontManager = new FontManagerImpl();
var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic));
var skTypeface = glyphTypeface.Typeface;
Assert.Equal("Noto Mono", skTypeface.FamilyName);
}
}
}
}

BIN
tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf

Binary file not shown.

5
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@ -7,6 +7,9 @@
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.UnitTests\Assets\*.ttf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
@ -20,7 +23,7 @@
<ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
</ItemGroup>
<Import Project="..\..\src\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\src\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
</Project>

35
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using Moq;
namespace Avalonia.UnitTests
{
public class MockFontManagerImpl : IFontManagerImpl
{
public string GetDefaultFontFamilyName()
{
return "Default";
}
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
return new[] { "Default" };
}
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
CultureInfo culture, out FontKey fontKey)
{
fontKey = default;
return false;
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{
return Mock.Of<IGlyphTypefaceImpl>();
}
}
}

47
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@ -0,0 +1,47 @@
using System;
using Avalonia.Platform;
namespace Avalonia.UnitTests
{
public class MockGlyphTypeface : IGlyphTypefaceImpl
{
public short DesignEmHeight => 10;
public int Ascent => 100;
public int Descent => 0;
public int LineGap { get; }
public int UnderlinePosition { get; }
public int UnderlineThickness { get; }
public int StrikethroughPosition { get; }
public int StrikethroughThickness { get; }
public bool IsFixedPitch { get; }
public ushort GetGlyph(uint codepoint)
{
return 0;
}
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
{
return new ushort[codepoints.Length];
}
public int GetGlyphAdvance(ushort glyph)
{
return 100;
}
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
{
var advances = new int[glyphs.Length];
for (var i = 0; i < advances.Length; i++)
{
advances[i] = 100;
}
return advances;
}
public void Dispose() { }
}
}

10
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -79,9 +79,15 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
public IFontManagerImpl CreateFontManager()
{
return Mock.Of<IGlyphTypefaceImpl>();
return new MockFontManagerImpl();
}
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
width = 0;
return Mock.Of<IGlyphRunImpl>();
}
}
}

25
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@ -0,0 +1,25 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class FontManagerTests
{
[Fact]
public void Should_Create_Single_Instance_Typeface()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new MockPlatformRenderInterface());
var fontFamily = new FontFamily("MyFont");
var typeface = FontManager.Current.GetOrAddTypeface(fontFamily);
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
}
}
}
}

130
tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

@ -0,0 +1,130 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class GlyphRunTests : TestWithServicesBase
{
public GlyphRunTests()
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToSingleton<MockPlatformRenderInterface>();
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 3, 30)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 1, 0, 10)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 0, 20)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 1, 30)]
[Theory]
public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance)
{
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var characterHit = new CharacterHit(start, trailingLength);
var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
Assert.Equal(expectedDistance, distance);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 25.0, 0, 3, true)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 20.0, 2, 0, true)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 26.0, 2, 1, true)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 35.0, 2, 1, false)]
[Theory]
public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
int trailingLengthExpected, bool isInsideExpected)
{
using (var glyphRun = CreateGlyphRun(advances, clusters))
{
var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
Assert.Equal(start, textBounds.FirstCharacterIndex);
Assert.Equal(trailingLengthExpected, textBounds.TrailingLength);
Assert.Equal(isInsideExpected, isInside);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 30.0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 0, 1, 1, 1, 10.0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 2, 1, 2, 20.0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 1, 1, 2, 20.0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 1, 1, 0 }, 1, 1, 1, 2, 20.0)]
[Theory]
public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
int index, int expectedIndex, int expectedLength, double expectedWidth)
{
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
Assert.Equal(expectedIndex, textBounds.FirstCharacterIndex);
Assert.Equal(expectedLength, textBounds.TrailingLength);
Assert.Equal(expectedWidth, width, 2);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 1)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 0, 3, 1, 0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 0, 3, 1, 1)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 0, 4, 1, 0)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 0, 4, 1, 1)]
[Theory]
public void Should_Get_Next_CharacterHit(double[] advances, ushort[] clusters,
int currentIndex, int currentLength,
int nextIndex, int nextLength,
int bidiLevel)
{
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
Assert.Equal(nextIndex, characterHit.FirstCharacterIndex);
Assert.Equal(nextLength, characterHit.TrailingLength);
}
}
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 0)]
[InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 1)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 1, 3, 0, 0)]
[InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 1, 3, 0, 1)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 1, 4, 0, 0)]
[InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 1, 4, 0, 1)]
[Theory]
public void Should_Get_Previous_CharacterHit(double[] advances, ushort[] clusters,
int currentIndex, int currentLength,
int previousIndex, int previousLength,
int bidiLevel)
{
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);
Assert.Equal(previousLength, characterHit.TrailingLength);
}
}
private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0)
{
var count = glyphAdvances.Length;
var glyphIndices = new ushort[count];
var bounds = new Rect(0, 0, count * 10, 10);
return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances,
glyphClusters: glyphClusters, bidiLevel: bidiLevel, bounds: bounds);
}
}
}

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

@ -3,13 +3,12 @@ using System.Collections.Generic;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
namespace Avalonia.Visuals.UnitTests.VisualTree
{
class MockRenderInterface : IPlatformRenderInterface
{
public IEnumerable<string> InstalledFontNames => new string[0];
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
@ -52,11 +51,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
throw new NotImplementedException();
}
public IFontManagerImpl CreateFontManager()
{
return new MockFontManagerImpl();
}
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt)
{
throw new NotImplementedException();

Loading…
Cancel
Save