diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index f8767c7599..873048ef21 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 796bd8e596..08a9aa3ceb 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 3eb6d5b595..d50b051d9f 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/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("fontComboBox"); - fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies; + fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } } diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 7f63e7725f..b17520a466 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -41,6 +41,9 @@ + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml new file mode 100644 index 0000000000..fb3e318a0e --- /dev/null +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs new file mode 100644 index 0000000000..7f15845596 --- /dev/null +++ b/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"); + + 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; + } + } +} diff --git a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs new file mode 100644 index 0000000000..a1246c57b5 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs @@ -0,0 +1,48 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + public struct ImmutableReadOnlyListStructEnumerator : IEnumerator, IEnumerator + { + private readonly IReadOnlyList _readOnlyList; + private int _pos; + + public ImmutableReadOnlyListStructEnumerator(IReadOnlyList 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; + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index cd14211075..d27de7a80d 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/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(); } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e0cc9aa128..9084012619 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/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, diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c7855ddfd1..8b8c7285be 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/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, diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 77902a7390..a9ce8ee494 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/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 /// /// The focus scopes in which the focus is currently defined. /// - private readonly Dictionary _focusScopes = - new Dictionary(); + private readonly ConditionalWeakTable _focusScopes = + new ConditionalWeakTable(); /// /// Initializes a new instance of the class. @@ -110,7 +111,18 @@ namespace Avalonia.Input { Contract.Requires(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) { diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 92b7dae904..d05dbac574 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -306,7 +306,7 @@ namespace Avalonia /// /// Parses a string. /// - /// The string. + /// Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new /// The . public static Matrix Parse(string s) { diff --git a/src/Avalonia.Visuals/Media/CharacterHit.cs b/src/Avalonia.Visuals/Media/CharacterHit.cs new file mode 100644 index 0000000000..978a5b0c4c --- /dev/null +++ b/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 +{ + /// + /// Represents information about a character hit within a glyph run. + /// + /// + /// The CharacterHit structure provides information about the index of the first + /// character that got hit as well as information about leading or trailing edge. + /// + public readonly struct CharacterHit : IEquatable + { + /// + /// Initializes a new instance of the structure. + /// + /// Index of the first character that got hit. + /// 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. + public CharacterHit(int firstCharacterIndex, int trailingLength = 0) + { + FirstCharacterIndex = firstCharacterIndex; + + TrailingLength = trailingLength; + } + + /// + /// Gets the index of the first character that got hit. + /// + public int FirstCharacterIndex { get; } + + /// + /// Gets the trailing length value for the character that got hit. + /// + 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); + } + } +} diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 8aa0bac41a..df69ab6fd5 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -187,6 +187,22 @@ namespace Avalonia.Media } } + /// + /// Draws a glyph run. + /// + /// The foreground brush. + /// The glyph run. + /// The baseline origin of the glyph run. + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + Contract.Requires(glyphRun != null); + + if (foreground != null) + { + PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin); + } + } + /// /// Draws a filled rectangle. /// diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index 771de524d9..a69a93e416 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/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); } /// @@ -57,15 +57,6 @@ namespace Avalonia.Media /// public static FontFamily Default { get; } - /// - /// Represents all font families in the system. This can be an expensive call depending on platform implementation. - /// - /// - /// Consider using the new instead. - /// - public static IEnumerable SystemFontFamilies => - FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name)); - /// /// Gets the primary family name of the font family. /// @@ -86,10 +77,16 @@ namespace Avalonia.Media /// Gets the key for associated assets. /// /// - /// The family familyNames. + /// The family key. /// + /// Key is only used for custom fonts. public FontFamilyKey Key { get; } + /// + /// Returns True if this instance is the system's default. + /// + public bool IsDefault => Name.Equals(DefaultFontFamilyName); + /// /// Implicit conversion of string to FontFamily /// @@ -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)) diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index be1bd269ed..0c5e88b47a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/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. /// - public abstract class FontManager + public sealed class FontManager { - public static readonly FontManager Default = CreateDefault(); + private readonly ConcurrentDictionary _typefaceCache = + new ConcurrentDictionary(); + 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(); + + if (current != null) + { + return current; + } + + var renderInterface = AvaloniaLocator.Current.GetService(); + + var fontManagerImpl = renderInterface?.CreateFontManager(); + + if (fontManagerImpl == null) + { + return null; + } + + current = new FontManager(fontManagerImpl); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); + + return current; + } + } + + /// + /// + /// + public IFontManagerImpl PlatformImpl { get; } /// /// Gets the system's default font family's name. @@ -21,25 +67,55 @@ namespace Avalonia.Media public string DefaultFontFamilyName { get; - protected set; } /// - /// Get all installed fonts in the system. - /// If true the font collection is updated. + /// Get all installed font family names. /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + /// If true the font collection is updated. + public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => + PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); /// - /// Get a cached typeface from specified parameters. + /// Returns a new typeface, or an existing one if a matching typeface exists. /// /// The font family. /// The font weight. /// The font style. /// - /// The cached typeface. + /// The typeface. /// - 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; + } + } /// /// Tries to match a specified character to a typeface that supports specified font properties. @@ -53,60 +129,13 @@ namespace Avalonia.Media /// /// The matched typeface. /// - 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(); - - 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 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 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; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index eb0faf4187..cd08bba7b2 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/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 /// internal IReadOnlyList Names { get; } - /// /// - /// Returns an enumerator that iterates through the collection. + /// Returns an enumerator for the name collection. /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() + public ImmutableReadOnlyListStructEnumerator GetEnumerator() { - return Names.GetEnumerator(); + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } - /// - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// 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); + } + /// /// Determines whether the specified , is equal to this instance. /// diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs index 7733dd7d2a..887862face 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs +++ b/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); + } + /// /// Determines whether the specified , is equal to this instance. /// diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs index 063fe8f20d..bed1fc6b83 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs +++ b/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(); - } - /// /// Loads all font assets that belong to the specified /// @@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts /// private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey) { - var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri); + var assetLoader = AvaloniaLocator.Current.GetService(); + + 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 /// private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey) { + var assetLoader = AvaloniaLocator.Current.GetService(); + 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; diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs similarity index 65% rename from src/Skia/Avalonia.Skia/FontKey.cs rename to src/Avalonia.Visuals/Media/Fonts/FontKey.cs index bb3fe230c1..0ead585612 100644 --- a/src/Skia/Avalonia.Skia/FontKey.cs +++ b/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 + public readonly struct FontKey : IEquatable { + 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; } } diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs new file mode 100644 index 0000000000..43151deece --- /dev/null +++ b/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 +{ + /// + /// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style. + /// + public sealed class GlyphRun : IDisposable + { + private static readonly IPlatformRenderInterface s_platformRenderInterface = + AvaloniaLocator.Current.GetService(); + + private IGlyphRunImpl _glyphRunImpl; + private GlyphTypeface _glyphTypeface; + private double _fontRenderingEmSize; + private Rect? _bounds; + + private ReadOnlySlice _glyphIndices; + private ReadOnlySlice _glyphAdvances; + private ReadOnlySlice _glyphOffsets; + private ReadOnlySlice _glyphClusters; + private ReadOnlySlice _characters; + + /// + /// Initializes a new instance of the class. + /// + public GlyphRun() + { + + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The glyph indices. + /// The glyph advances. + /// The glyph offsets. + /// The characters. + /// The glyph clusters. + /// The bidi level. + /// The bound. + public GlyphRun( + GlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlySlice glyphIndices, + ReadOnlySlice glyphAdvances = default, + ReadOnlySlice glyphOffsets = default, + ReadOnlySlice characters = default, + ReadOnlySlice 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); + } + + /// + /// Gets or sets the for the . + /// + public GlyphTypeface GlyphTypeface + { + get => _glyphTypeface; + set => Set(ref _glyphTypeface, value); + } + + /// + /// Gets or sets the em size used for rendering the . + /// + public double FontRenderingEmSize + { + get => _fontRenderingEmSize; + set => Set(ref _fontRenderingEmSize, value); + } + + /// + /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. + /// + public ReadOnlySlice GlyphIndices + { + get => _glyphIndices; + set => Set(ref _glyphIndices, value); + } + + /// + /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. + /// + public ReadOnlySlice GlyphAdvances + { + get => _glyphAdvances; + set => Set(ref _glyphAdvances, value); + } + + /// + /// Gets or sets an array of values representing the offsets of the glyphs in the . + /// + public ReadOnlySlice GlyphOffsets + { + get => _glyphOffsets; + set => Set(ref _glyphOffsets, value); + } + + /// + /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . + /// + public ReadOnlySlice Characters + { + get => _characters; + set => Set(ref _characters, value); + } + + /// + /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// + public ReadOnlySlice GlyphClusters + { + get => _glyphClusters; + set => Set(ref _glyphClusters, value); + } + + /// + /// Gets or sets the bidirectional nesting level of the . + /// + public int BidiLevel + { + get; + set; + } + + /// + /// + /// + internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight; + + /// + /// + /// + internal bool IsLeftToRight => ((BidiLevel & 1) == 0); + + /// + /// Gets or sets the conservative bounding box of the . + /// + 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 : IComparer + { + public int Compare(T x, T y) + { + return Comparer.Default.Compare(y, x); + } + } + + private static readonly IComparer s_ascendingComparer = Comparer.Default; + private static readonly IComparer s_descendingComparer = new ReverseComparer(); + + 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(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(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs new file mode 100644 index 0000000000..22d6e20b34 --- /dev/null +++ b/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 ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); + + public static readonly StyledProperty GlyphRunProperty = + AvaloniaProperty.Register(nameof(GlyphRun)); + + public static readonly StyledProperty BaselineOriginProperty = + AvaloniaProperty.Register(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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 3ba31f7e84..6468f701d6 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/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(); - - 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 /// public int StrikethroughThickness => PlatformImpl.StrikethroughThickness; + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + public bool IsFixedPitch => PlatformImpl.IsFixedPitch; + /// /// Returns an glyph index for the specified codepoint. /// diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 7a70657ce0..8253d11ff1 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -28,6 +28,16 @@ namespace Avalonia.Media /// public abstract Matrix Value { get; } + /// + /// Parses a string. + /// + /// Six comma-delimited double values that describe the new . For details check + /// The . + public static Transform Parse(string s) + { + return new MatrixTransform(Matrix.Parse(s)); + } + /// /// Raises the event. /// @@ -35,5 +45,14 @@ namespace Avalonia.Media { Changed?.Invoke(this, EventArgs.Empty); } + + /// + /// Returns a String representing this transform matrix instance. + /// + /// The string representation. + public override string ToString() + { + return Value.ToString(); + } } } diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index a6d5c8a43c..9a17bad7d2 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/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 { - public static readonly Typeface Default = new Typeface(FontFamily.Default); - private GlyphTypeface _glyphTypeface; /// @@ -50,6 +48,8 @@ namespace Avalonia.Media { } + public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); + /// /// Gets the font family. /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 5edb1c9760..f2309c271d 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -86,6 +86,14 @@ namespace Avalonia.Platform /// The text. void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text); + /// + /// Draws a glyph run. + /// + /// The foreground. + /// The glyph run. + /// The baseline origin of the glyph run. + void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin); + /// /// Creates a new that can be used as a render layer /// for the current render target. diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 254b5d07d1..a8e6dcb29b 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/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 /// /// Gets the system's default font family's name. /// - string DefaultFontFamilyName { get; } + string GetDefaultFontFamilyName(); /// /// Get all installed fonts in the system. @@ -20,17 +21,6 @@ namespace Avalonia.Platform /// IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); - /// - /// Get a typeface from specified parameters. - /// - /// The font family. - /// The font weight. - /// The font style. - /// - /// The typeface. - /// - Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle); - /// /// Tries to match a specified character to a typeface that supports specified font properties. /// @@ -39,10 +29,20 @@ namespace Avalonia.Platform /// The font style. /// The font family. This is optional and used for fallback lookup. /// The culture. + /// The matching font key. /// - /// The typeface. + /// True, if the could match the character to specified parameters, False otherwise. + /// + bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); + + /// + /// Creates a glyph typeface. + /// + /// The typeface. + /// 0 + /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface. /// - Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default, - FontFamily fontFamily = null, CultureInfo culture = null); + IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); } } diff --git a/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs new file mode 100644 index 0000000000..0f1359794a --- /dev/null +++ b/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 +{ + /// + /// Actual implementation of a glyph run that stores platform dependent resources. + /// + public interface IGlyphRunImpl : IDisposable { } +} diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs index 8c043a5129..5d6ff23c0a 100644 --- a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs @@ -47,6 +47,11 @@ namespace Avalonia.Platform /// int StrikethroughThickness { get; } + /// + /// A value indicating whether all glyphs in the font have the same advancement. + /// + bool IsFixedPitch { get; } + /// /// Returns an glyph index for the specified codepoint. /// diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 5a0a7b2f19..7ae0eaf8f2 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/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); /// - /// Creates a glyph typeface for specified typeface. + /// Creates a font manager implementation. /// - /// The typeface. - /// - /// The glyph typeface implementation. - /// - IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface); + /// The font manager. + IFontManagerImpl CreateFontManager(); + + /// + /// Creates a platform implementation of a glyph run. + /// + /// The glyph run. + /// The glyph run's width. + /// + IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width); } } diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index d92f8b0fc4..27ac7a3026 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -175,7 +175,7 @@ namespace Avalonia /// Parses a string. /// /// The string. - /// The . + /// The . public static Point Parse(string s) { using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point.")) diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index 2e8fb16bc1..ebd0ba9351 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.cs @@ -177,5 +177,16 @@ namespace Avalonia unit); } } + + /// + /// Returns a String representing this RelativePoint instance. + /// + /// The string representation. + public override string ToString() + { + return _unit == RelativeUnit.Absolute ? + _point.ToString() : + string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100); + } } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index e39581fc57..1e7b5c2923 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/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 }; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 4fbfb02660..a169a629be 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -190,6 +190,21 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) + { + var next = NextDrawAs(); + + 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"); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs new file mode 100644 index 0000000000..b3c4fdbac0 --- /dev/null +++ b/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 +{ + /// + /// A node in the scene graph which represents a glyph run draw. + /// + internal class GlyphRunNode : BrushDrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The foreground brush. + /// The glyph run to draw. + /// The baseline origin of the glyph run. + /// Child scenes for drawing visual brushes. + public GlyphRunNode( + Matrix transform, + IBrush foreground, + GlyphRun glyphRun, + Point baselineOrigin, + IDictionary childScenes = null) + : base(glyphRun.Bounds, transform, null) + { + Transform = transform; + Foreground = foreground?.ToImmutable(); + GlyphRun = glyphRun; + BaselineOrigin = baselineOrigin; + ChildScenes = childScenes; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + /// + /// Gets the foreground brush. + /// + public IBrush Foreground { get; } + + /// + /// Gets the glyph run to draw. + /// + public GlyphRun GlyphRun { get; } + + /// + /// Gets the baseline origin. + /// + public Point BaselineOrigin { get; set; } + + /// + public override IDictionary ChildScenes { get; } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin); + } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The foreground of the other draw operation. + /// The glyph run of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) + { + return transform == Transform && + Equals(foreground, Foreground) && + Equals(glyphRun, GlyphRun); + } + + /// + public override bool HitTest(Point p) => Bounds.Contains(p); + } +} diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs new file mode 100644 index 0000000000..c54ccc8ef1 --- /dev/null +++ b/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 +{ + /// + /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. + /// + /// The type of elements in the slice. + public readonly struct ReadOnlySlice : IReadOnlyList + { + public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } + + public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length) + { + Buffer = buffer; + Start = start; + Length = length; + } + + /// + /// Gets the start. + /// + /// + /// The start. + /// + public int Start { get; } + + /// + /// Gets the end. + /// + /// + /// The end. + /// + public int End => Start + Length - 1; + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public int Length { get; } + + /// + /// Gets a value that indicates whether this instance of is Empty. + /// + public bool IsEmpty => Length == 0; + + /// + /// The buffer. + /// + public ReadOnlyMemory Buffer { get; } + + public T this[int index] => Buffer.Span[Start + index]; + + /// + /// Returns a span of the underlying buffer. + /// + /// The of the underlying buffer. + public ReadOnlySpan AsSpan() + { + return Buffer.Span.Slice(Start, Length); + } + + /// + /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. + /// + /// The start of the sub slice. + /// The length of the sub slice. + /// A that contains the specified number of elements from the specified start. + public ReadOnlySlice 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(Buffer, Start + start, length); + } + + /// + /// Returns a specified number of contiguous elements from the start of the slice. + /// + /// The number of elements to return. + /// A that contains the specified number of elements from the start of this slice. + public ReadOnlySlice Take(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start, length); + } + + /// + /// Bypasses a specified number of elements in the slice and then returns the remaining elements. + /// + /// The number of elements to skip before returning the remaining elements. + /// A that contains the elements that occur after the specified index in this slice. + public ReadOnlySlice Skip(int length) + { + if (length > Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return new ReadOnlySlice(Buffer, Start + length, Length - length); + } + + /// + /// Returns an enumerator for the slice. + /// + public ImmutableReadOnlyListStructEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + int IReadOnlyCollection.Count => Length; + + T IReadOnlyList.this[int index] => this[index]; + + public static implicit operator ReadOnlySlice(T[] array) + { + return new ReadOnlySlice(array); + } + + public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) + { + return new ReadOnlySlice(memory); + } + } +} diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 576d2daaaa..d99fbe8e65 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/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); + /// + /// Parses a string. + /// + /// The string. + /// The . + public static Vector Parse(string s) + { + using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector.")) + { + return new Vector( + tokenizer.ReadDouble(), + tokenizer.ReadDouble() + ); + } + } + /// /// Length of the vector /// @@ -166,9 +183,9 @@ namespace Avalonia } /// - /// Returns the string representation of the point. + /// Returns the string representation of the vector. /// - /// The string representation of the point. + /// The string representation of the vector. public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index aab43bbd6f..40386924c3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/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 { diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index ad9915e193..4c4b6cf8ff 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit ad9915e19398a49c5a11b66000c361659ca692b3 +Subproject commit 4c4b6cf8ff0894c925d87b27d4fc7a064440c218 diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 835c377791..d06cfa69a7 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -232,6 +232,20 @@ namespace Avalonia.Skia } } + /// + 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); + } + } + /// public IRenderTargetBitmapImpl CreateLayer(Size size) { diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 03de82178a..727947e59d 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/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 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); } } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index a9358cb458..8effb94ca9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/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() }; diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs new file mode 100644 index 0000000000..e0f62d6085 --- /dev/null +++ b/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 +{ + /// + public class GlyphRunImpl : IGlyphRunImpl + { + public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) + { + Paint = paint; + TextBlob = textBlob; + } + + /// + /// Gets the paint to draw with. + /// + public SKPaint Paint { get; } + + /// + /// Gets the text blob to draw. + /// + public SKTextBlob TextBlob { get; } + + void IDisposable.Dispose() + { + TextBlob.Dispose(); + Paint.Dispose(); + } + } +} diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index e46f766255..bb2650a5c6 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/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 /// public int LineGap { get; } - //ToDo: Get these values from HarfBuzz /// public int UnderlinePosition { get; } @@ -94,6 +95,9 @@ namespace Avalonia.Skia /// public int StrikethroughThickness { get; } + /// + public bool IsFixedPitch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ee0cfb2f06..05c3bbdaa0 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/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 /// internal class PlatformRenderInterface : IPlatformRenderInterface { - private readonly ConcurrentDictionary _glyphTypefaceCache = - new ConcurrentDictionary(); - private readonly ICustomSkiaGpu _customSkiaGpu; private GRContext GrContext { get; } @@ -60,7 +56,7 @@ namespace Avalonia.Skia Size constraint, IReadOnlyList 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) + /// + public IFontManagerImpl CreateFontManager() { - return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface)); + return new FontManagerImpl(); + } + + /// + 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); + } } } } diff --git a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f6aabfae39 --- /dev/null +++ b/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")] diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 577567a8a1..d1c1961a8a 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/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> _fontFamilies = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary _typefaces = + new ConcurrentDictionary(); - 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(); - - _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 fontFamily, FontKey key) + private static SKTypeface GetNearestMatch(IDictionary 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]; } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4bb42c7118..71edae26df 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/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; diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index ce3aef755b..f16e967f42 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -25,11 +25,6 @@ namespace Avalonia.Skia AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface); - - var fontManager = new FontManagerImpl(); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(fontManager); } /// diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs deleted file mode 100644 index 1c2b855032..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ /dev/null @@ -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 -{ - /// - /// Cache for Skia typefaces. - /// - internal static class TypefaceCache - { - private static readonly ConcurrentDictionary> s_cache = - new ConcurrentDictionary>(); - - 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()); - - 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; - } - } -} diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs deleted file mode 100644 index ef9f889819..0000000000 --- a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs +++ /dev/null @@ -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; } - } -} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 1bda5157a5..a2bedf3190 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/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 _glyphTypefaceCache = - new ConcurrentDictionary(); 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().ToConstant(s_instance); - AvaloniaLocator.CurrentMutable.Bind().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) + /// + 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); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs index b455c4fbee..78bf25d607 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs +++ b/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); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 628f245ae5..aa13003643 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -316,6 +316,22 @@ namespace Avalonia.Direct2D1.Media } } + /// + /// Draws a glyph run. + /// + /// The foreground. + /// The glyph run. + /// + 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) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 94de397652..31604ad15f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/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 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); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index b1a177ad24..8e492a66ff 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/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; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs new file mode 100644 index 0000000000..0b06d5ef3e --- /dev/null +++ b/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(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs index 32def01c39..dfc3b48eaa 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs +++ b/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(); 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 /// public int StrikethroughThickness { get; } + /// + public bool IsFixedPitch { get; } + /// public ushort GetGlyph(uint codepoint) { diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs index a09d5c2d1c..26a8526c16 100644 --- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs +++ b/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")] + diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index df2508a3ed..6e87a90ea4 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/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(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = items, + ItemTemplate = new FuncDataTemplate((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((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) diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs new file mode 100644 index 0000000000..82471915f4 --- /dev/null +++ b/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().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().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)); + } + } + } +} diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index a683e5cfca..f063d59ca4 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests It.IsAny>())) .Returns(new FormattedTextMock("TEST")); + renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl()); + var streamGeometry = new Mock(); streamGeometry.Setup(x => x.Open()) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index f4d4a9dd2a..eaf9f22406 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/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 = @" + + + +"; + var template = AvaloniaXamlLoader.Parse(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)) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index b76022852c..f7629e5b9e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/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 = @" + + + + +"; + 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 = @" + + + + +"; + 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() { diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs index bca34dd69d..73e63ae2ac 100644 --- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs +++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs @@ -53,7 +53,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var r = AvaloniaLocator.Current.GetService(); return r.CreateFormattedText(text, - new Typeface(fontFamily, fontWeight, fontStyle), + FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle), fontSize, textAlignment, wrapping, diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs new file mode 100644 index 0000000000..927f98b32b --- /dev/null +++ b/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().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().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); + } + } + } +} diff --git a/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf new file mode 100644 index 0000000000..3560a3a0c8 Binary files /dev/null and b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf differ diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 272b1fc489..b1d89037da 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -7,6 +7,9 @@ false latest + + + @@ -20,7 +23,7 @@ + - diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs new file mode 100644 index 0000000000..faf6f98138 --- /dev/null +++ b/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 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(); + } + } +} diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs new file mode 100644 index 0000000000..93ff84d04a --- /dev/null +++ b/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 codepoints) + { + return new ushort[codepoints.Length]; + } + + public int GetGlyphAdvance(ushort glyph) + { + return 100; + } + + public int[] GetGlyphAdvances(ReadOnlySpan glyphs) + { + var advances = new int[glyphs.Length]; + + for (var i = 0; i < advances.Length; i++) + { + advances[i] = 100; + } + + return advances; + } + + public void Dispose() { } + } +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 187853283f..5da9f8ff6e 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/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(); + return new MockFontManagerImpl(); + } + + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + width = 0; + return Mock.Of(); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs new file mode 100644 index 0000000000..6cbab08905 --- /dev/null +++ b/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().ToConstant(new MockPlatformRenderInterface()); + + var fontFamily = new FontFamily("MyFont"); + + var typeface = FontManager.Current.GetOrAddTypeface(fontFamily); + + Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs new file mode 100644 index 0000000000..f5e4cdc099 --- /dev/null +++ b/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().ToSingleton(); + } + + [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); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 032b6582a9..28304b674b 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/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 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();