From ce5f884e0869e65ae5695c3b3fd743cdd706340e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Dec 2019 19:44:02 +0100 Subject: [PATCH 01/21] Added failing test for #3321. --- .../ListBoxTests.cs | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) 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) From e1b333881487345dd32aa726eadfb697003b17f6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Dec 2019 00:21:22 +0100 Subject: [PATCH 02/21] Fix for #3321. Remove containers if number of items is now less than the number of materialized containers. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(); } From 1b0221e2245b6772f12a239d0b6f582a9e68a2ca Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 4 Dec 2019 16:39:43 +0100 Subject: [PATCH 03/21] Reworked the font manager to better manage resoures and also work properly with the application lifetime --- .../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 4 +- .../Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 2 +- src/Avalonia.Visuals/Media/FontFamily.cs | 38 +++-- src/Avalonia.Visuals/Media/FontManager.cs | 151 +++++++++++------- .../Media/Fonts/FamilyNameCollection.cs | 80 ++++++++-- .../Media/Fonts/FontFamilyKey.cs | 15 ++ .../Media/Fonts/FontFamilyLoader.cs | 15 +- .../Media/Fonts}/FontKey.cs | 15 +- src/Avalonia.Visuals/Media/GlyphTypeface.cs | 6 +- src/Avalonia.Visuals/Media/Typeface.cs | 4 +- .../Platform/IFontManagerImpl.cs | 30 ++-- .../Platform/IPlatformRenderInterface.cs | 9 +- .../Rendering/RendererBase.cs | 3 +- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 77 ++++++--- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 4 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 5 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 11 +- .../Avalonia.Skia/Properties/AssemblyInfo.cs | 4 + .../Avalonia.Skia/SKTypefaceCollection.cs | 47 +++--- .../SKTypefaceCollectionCache.cs | 8 +- src/Skia/Avalonia.Skia/SkiaPlatform.cs | 5 - src/Skia/Avalonia.Skia/TypefaceCache.cs | 47 ------ .../Avalonia.Skia/TypefaceCollectionEntry.cs | 19 --- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 8 +- .../Media/Direct2D1FontCollectionCache.cs | 9 +- .../Media/FontManagerImpl.cs | 34 ++-- .../Media/FormattedTextImpl.cs | 2 +- .../Properties/AssemblyInfo.cs | 4 + .../Media/FontManagerImplTests.cs | 111 +++++++++++++ .../FullLayoutTests.cs | 2 + .../Media/FormattedTextImplTests.cs | 2 +- .../FontManagerImplTests.cs | 93 +++++++++++ .../Assets/NotoMono-Regular.ttf | Bin 0 -> 107848 bytes .../Avalonia.UnitTests.csproj | 5 +- .../Avalonia.UnitTests/MockFontManagerImpl.cs | 35 ++++ .../MockPlatformRenderInterface.cs | 4 +- .../Media/FontManagerTests.cs | 25 +++ .../VisualTree/MockRenderInterface.cs | 7 +- 39 files changed, 641 insertions(+), 301 deletions(-) rename src/{Skia/Avalonia.Skia => Avalonia.Visuals/Media/Fonts}/FontKey.cs (65%) create mode 100644 src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs delete mode 100644 src/Skia/Avalonia.Skia/TypefaceCache.cs delete mode 100644 src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs create mode 100644 tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs create mode 100644 tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs create mode 100644 tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf create mode 100644 tests/Avalonia.UnitTests/MockFontManagerImpl.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs 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/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.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..27ed9e64da 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. + /// Get all installed fonts. /// If true the font collection is updated. /// - public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false); + 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 - { - 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 Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + FontStyle fontStyle = FontStyle.Normal, + FontFamily fontFamily = null, CultureInfo culture = null) { - 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..8d2fd076c8 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -54,30 +54,67 @@ 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 Enumerator GetEnumerator() { - return Names.GetEnumerator(); + return new Enumerator(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(); } + public struct Enumerator : IEnumerator, IEnumerator + { + private readonly IReadOnlyList _names; + private int _pos; + + public Enumerator(IReadOnlyList names) + { + _names = names; + _pos = -1; + Current = default; + } + + public string Current + { + get; + private set; + } + + object IEnumerator.Current => Current; + + public void Dispose() { } + + public bool MoveNext() + { + if (_pos >= _names.Count - 1) + { + return false; + } + + Current = _names[++_pos]; + + return true; + + } + + public void Reset() + { + _pos = -1; + + Current = default; + } + } + /// /// Returns a that represents this instance. /// @@ -131,6 +168,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/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs index 3ba31f7e84..b03cf5908a 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) 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/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/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index 5a0a7b2f19..edde10358c 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -113,12 +113,9 @@ 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(); } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index e341f02901..014101828f 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/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 03de82178a..9cfa685191 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[] s_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 (s_languageTagBuffer == null) + { + s_languageTagBuffer = new string[2]; + } + + s_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; + s_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, s_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, s_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/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index e46f766255..d4dc70e808 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) { @@ -81,7 +81,6 @@ namespace Avalonia.Skia /// public int LineGap { get; } - //ToDo: Get these values from HarfBuzz /// public int UnderlinePosition { get; } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ee0cfb2f06..e17d6fdce3 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,10 @@ 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(); } } } 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..4068b31c9a 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -28,8 +28,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 +107,6 @@ namespace Avalonia.Direct2D1 { InitializeDirect2D(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl()); SharpDX.Configuration.EnableReleaseOnFinalizer = true; } @@ -194,9 +191,10 @@ 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(); } } } 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/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..949bf2be70 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media { Text = text; - using (var font = Direct2D1FontCollectionCache.GetFont(typeface)) + var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont; 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)) 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.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.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 0000000000000000000000000000000000000000..3560a3a0c87b328a63c3c5dcd2020f3fc32dbbe9 GIT binary patch literal 107848 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%0Am6uY2R}KQC=> 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

#cf7b!d>yWeyc2A*xN`Qh-!Gha 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 ke@Vs-7zxz!&}3uAWBfalse 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/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 187853283f..ba436405ce 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -79,9 +79,9 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + public IFontManagerImpl CreateFontManager() { - return Mock.Of(); + return new MockFontManagerImpl(); } } } 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/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 032b6582a9..5cd313b169 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -8,8 +8,6 @@ namespace Avalonia.Visuals.UnitTests.VisualTree { class MockRenderInterface : IPlatformRenderInterface { - public IEnumerable InstalledFontNames => new string[0]; - public IFormattedTextImpl CreateFormattedText( string text, Typeface typeface, @@ -57,6 +55,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } + public IFontManagerImpl CreateFontManager() + { + throw new NotImplementedException(); + } + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt) { throw new NotImplementedException(); From c57bf0a1fe5d2c2470ad38c44b676266e3daf3cf Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 4 Dec 2019 18:43:50 +0100 Subject: [PATCH 04/21] Fix FormattedTextImpl with fallbacks --- .../Avalonia.Direct2D1/Media/FormattedTextImpl.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index 949bf2be70..8e492a66ff 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -23,9 +23,15 @@ namespace Avalonia.Direct2D1.Media Text = text; var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont; - 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 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; From e704823ced4edac89a38ec1be916108bc6ebb055 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 6 Dec 2019 16:11:01 +0100 Subject: [PATCH 05/21] Introduce a struct enumerator implementation for ReadOnlyList that can be reused --- .../ImmutableReadOnlyListStructEnumerator.cs | 48 +++++++++++++++++++ src/Avalonia.Visuals/Media/FontManager.cs | 4 +- .../Media/Fonts/FamilyNameCollection.cs | 48 ++----------------- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 14 +++--- .../VisualTree/MockRenderInterface.cs | 3 +- 5 files changed, 62 insertions(+), 55 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs 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.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index 27ed9e64da..0c5e88b47a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -70,9 +70,9 @@ namespace Avalonia.Media } ///

- /// Get all installed fonts. - /// If true the font collection is updated. + /// Get all installed font family names. /// + /// If true the font collection is updated. public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) => PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index 8d2fd076c8..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 { @@ -57,9 +58,9 @@ namespace Avalonia.Media.Fonts /// /// Returns an enumerator for the name collection. /// - public Enumerator GetEnumerator() + public ImmutableReadOnlyListStructEnumerator GetEnumerator() { - return new Enumerator(this); + return new ImmutableReadOnlyListStructEnumerator(this); } IEnumerator IEnumerable.GetEnumerator() @@ -72,49 +73,6 @@ namespace Avalonia.Media.Fonts return GetEnumerator(); } - public struct Enumerator : IEnumerator, IEnumerator - { - private readonly IReadOnlyList _names; - private int _pos; - - public Enumerator(IReadOnlyList names) - { - _names = names; - _pos = -1; - Current = default; - } - - public string Current - { - get; - private set; - } - - object IEnumerator.Current => Current; - - public void Dispose() { } - - public bool MoveNext() - { - if (_pos >= _names.Count - 1) - { - return false; - } - - Current = _names[++_pos]; - - return true; - - } - - public void Reset() - { - _pos = -1; - - Current = default; - } - } - /// /// Returns a that represents this instance. /// diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 9cfa685191..727947e59d 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -30,7 +30,7 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - [ThreadStatic] private static string[] s_languageTagBuffer; + [ThreadStatic] private static string[] t_languageTagBuffer; public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) @@ -40,20 +40,20 @@ namespace Avalonia.Skia culture = CultureInfo.CurrentUICulture; } - if (s_languageTagBuffer == null) + if (t_languageTagBuffer == null) { - s_languageTagBuffer = new string[2]; + t_languageTagBuffer = new string[2]; } - s_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; - s_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; + 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, s_languageTagBuffer, codepoint); + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); if (skTypeface == null) { @@ -68,7 +68,7 @@ namespace Avalonia.Skia else { var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, s_languageTagBuffer, codepoint); + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); if (skTypeface != null) { diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 5cd313b169..300c6e359e 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.UnitTests; namespace Avalonia.Visuals.UnitTests.VisualTree { @@ -57,7 +58,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree public IFontManagerImpl CreateFontManager() { - throw new NotImplementedException(); + return new MockFontManagerImpl(); } public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt) From ab5e062deb517972e49e4e7c11cdf9534d9c39fa Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 6 Dec 2019 21:12:46 +0100 Subject: [PATCH 06/21] Add GlyphRun support --- build/HarfBuzzSharp.props | 4 +- build/SkiaSharp.props | 4 +- samples/RenderDemo/MainWindow.xaml | 3 + samples/RenderDemo/Pages/GlyphRunPage.xaml | 14 + samples/RenderDemo/Pages/GlyphRunPage.xaml.cs | 80 +++ src/Avalonia.Visuals/Media/CharacterHit.cs | 68 +++ src/Avalonia.Visuals/Media/DrawingContext.cs | 16 + src/Avalonia.Visuals/Media/GlyphRun.cs | 459 ++++++++++++++++++ src/Avalonia.Visuals/Media/GlyphRunDrawing.cs | 50 ++ src/Avalonia.Visuals/Media/GlyphTypeface.cs | 5 + .../Platform/IDrawingContextImpl.cs | 8 + .../Platform/IGlyphRunImpl.cs | 12 + .../Platform/IGlyphTypefaceImpl.cs | 5 + .../Platform/IPlatformRenderInterface.cs | 8 + .../SceneGraph/DeferredDrawingContextImpl.cs | 15 + .../Rendering/SceneGraph/GlyphRunNode.cs | 91 ++++ src/Avalonia.Visuals/Utility/ReadOnlySlice.cs | 154 ++++++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 14 + src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 35 ++ src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 5 + .../Avalonia.Skia/PlatformRenderInterface.cs | 85 ++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 50 ++ .../Media/DrawingContextImpl.cs | 16 + .../Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 19 + .../Media/GlyphTypefaceImpl.cs | 9 +- tests/Avalonia.UnitTests/MockGlyphTypeface.cs | 47 ++ .../MockPlatformRenderInterface.cs | 6 + .../Media/GlyphRunTests.cs | 112 +++++ .../VisualTree/MockRenderInterface.cs | 2 +- 29 files changed, 1389 insertions(+), 7 deletions(-) create mode 100644 samples/RenderDemo/Pages/GlyphRunPage.xaml create mode 100644 samples/RenderDemo/Pages/GlyphRunPage.xaml.cs create mode 100644 src/Avalonia.Visuals/Media/CharacterHit.cs create mode 100644 src/Avalonia.Visuals/Media/GlyphRun.cs create mode 100644 src/Avalonia.Visuals/Media/GlyphRunDrawing.cs create mode 100644 src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs create mode 100644 src/Avalonia.Visuals/Utility/ReadOnlySlice.cs create mode 100644 src/Skia/Avalonia.Skia/GlyphRunImpl.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs create mode 100644 tests/Avalonia.UnitTests/MockGlyphTypeface.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs 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/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.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/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs new file mode 100644 index 0000000000..a5e70ae2b1 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -0,0 +1,459 @@ +// 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 = _glyphClusters.AsSpan().BinarySearch((ushort)characterHit.FirstCharacterIndex); + + if (end < 0) + { + return 0; + } + + // If TrailingLength > 0 we have to use the next cluster while TrailingLength != 0 + for (var i = 0; i < end + characterHit.TrailingLength; i++) + { + 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 is 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 b03cf5908a..6468f701d6 100644 --- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs +++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs @@ -66,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/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/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 edde10358c..7ae0eaf8f2 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -117,5 +117,13 @@ namespace Avalonia.Platform ///
/// 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/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..b862dc218f --- /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 text 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 text 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 text 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/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/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 d4dc70e808..bb2650a5c6 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -61,6 +61,8 @@ namespace Avalonia.Skia { StrikethroughThickness = strikethroughThickness; } + + IsFixedPitch = Typeface.IsFixedPitch; } public Face Face { get; } @@ -93,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 e17d6fdce3..05c3bbdaa0 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -156,5 +156,90 @@ namespace Avalonia.Skia { 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/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 4068b31c9a..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 { @@ -196,5 +199,52 @@ namespace Avalonia.Direct2D1 { 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/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/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/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 ba436405ce..5da9f8ff6e 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -83,5 +83,11 @@ namespace Avalonia.UnitTests { return new MockFontManagerImpl(); } + + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) + { + width = 0; + return Mock.Of(); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs new file mode 100644 index 0000000000..c0820f2046 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs @@ -0,0 +1,112 @@ +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 }, 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_TextBounds_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 300c6e359e..28304b674b 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -51,7 +51,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) + public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { throw new NotImplementedException(); } From 7901f6f09f8816686e584c4c83b63df8f51769ed Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 6 Dec 2019 22:13:49 +0100 Subject: [PATCH 07/21] Fix some comments and GlyphRun.GetDistanceFromCharacterHit --- src/Avalonia.Visuals/Media/GlyphRun.cs | 16 +++++++-------- .../Rendering/SceneGraph/GlyphRunNode.cs | 6 +++--- .../Media/GlyphRunTests.cs | 20 ++++++++++++++++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index a5e70ae2b1..43151deece 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -193,16 +193,15 @@ namespace Avalonia.Media { var distance = 0.0; - var end = _glyphClusters.AsSpan().BinarySearch((ushort)characterHit.FirstCharacterIndex); + var end = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - if (end < 0) + for (var i = 0; i < _glyphClusters.Length; i++) { - return 0; - } + if (_glyphClusters[i] >= end) + { + break; + } - // If TrailingLength > 0 we have to use the next cluster while TrailingLength != 0 - for (var i = 0; i < end + characterHit.TrailingLength; i++) - { if (GlyphAdvances.IsEmpty) { var glyph = GlyphIndices[i]; @@ -279,7 +278,6 @@ namespace Avalonia.Media public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (characterHit.TrailingLength == 0) { return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); @@ -412,7 +410,7 @@ namespace Avalonia.Media { if (_glyphRunImpl != null) { - throw new InvalidOperationException("GlyphRun can't be changed after is has been initialized.'"); + throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'"); } field = value; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs index b862dc218f..b3c4fdbac0 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { /// - /// A node in the scene graph which represents a text draw. + /// A node in the scene graph which represents a glyph run draw. /// internal class GlyphRunNode : BrushDrawOperation { @@ -48,7 +48,7 @@ namespace Avalonia.Rendering.SceneGraph public IBrush Foreground { get; } /// - /// Gets the text to draw. + /// Gets the glyph run to draw. /// public GlyphRun GlyphRun { get; } @@ -72,7 +72,7 @@ namespace Avalonia.Rendering.SceneGraph ///
/// The transform of the other draw operation. /// The foreground of the other draw operation. - /// The text 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 diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs index c0820f2046..f5e4cdc099 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs @@ -13,12 +13,30 @@ namespace Avalonia.Visuals.UnitTests.Media .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_TextBounds_FromDistance(double[] advances, ushort[] clusters, double distance, int start, + public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start, int trailingLengthExpected, bool isInsideExpected) { using (var glyphRun = CreateGlyphRun(advances, clusters)) From 04415f2d54d35626761ea499bb9f708bf14e64a9 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 6 Dec 2019 23:22:31 +0200 Subject: [PATCH 08/21] fix typo of point parse --- src/Avalonia.Visuals/Point.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.")) From 30eaf3bc72ac0bc10c44050a4ffe0256f24888dc Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 6 Dec 2019 23:23:33 +0200 Subject: [PATCH 09/21] add vector parse --- src/Avalonia.Visuals/Vector.cs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) 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); From 7a66d360f043b179f5c80d86ef9daeeff579f2be Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 6 Dec 2019 23:38:46 +0200 Subject: [PATCH 10/21] add transform parse --- src/Avalonia.Visuals/Media/Transform.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 7a70657ce0..30a07d47ef 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. + /// + /// The string. + /// 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(); + } } } From 29be26a88240a0bc2b533dd5096495bac10b5305 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 6 Dec 2019 23:48:43 +0200 Subject: [PATCH 11/21] add relativepoint tostring --- src/Avalonia.Visuals/RelativePoint.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) 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); + } } } From 24c5519e7199f9b7631c25d7b75876ee69669aec Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 7 Dec 2019 18:31:19 +0100 Subject: [PATCH 12/21] Use CondtionalWeakTable to avoid FocusManager becoming a GC root for all focus scopes. --- src/Avalonia.Input/FocusManager.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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) { From a04d0b7bbb8b69416e566069c7f908f3c71dc3fb Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 8 Dec 2019 00:09:22 +0200 Subject: [PATCH 13/21] add failing test for ControlTemplate don't respect TargetType --- .../Xaml/BasicTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index f4d4a9dd2a..e6032b7c79 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() { From 97f57c7beceefc4449a15566d351d7b3654758d1 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 8 Dec 2019 00:11:59 +0200 Subject: [PATCH 14/21] add test for style resources --- .../Xaml/StyleTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index b76022852c..9c6fa6d10d 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,30 @@ 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 SolidColorBrush_Can_Be_Added_To_Style_Resources() { From 73be933bd017f77e2e4846a771c8c5d4d09c8425 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 8 Dec 2019 00:13:46 +0200 Subject: [PATCH 15/21] add failing test for ControlTemplate can't be added to resources #3145 --- .../Xaml/StyleTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 9c6fa6d10d..f7629e5b9e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -63,6 +63,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [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() { From b1cc2a1d9cc2307885dadc8f15f7d1a91d25eaa2 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 8 Dec 2019 00:17:56 +0200 Subject: [PATCH 16/21] enable valid xamlil test (disabled before because of portable.xaml) --- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index e6032b7c79..eaf9f22406 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -721,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)) From 64d8b4ef5b006fffc10be46bafc7d18b1415f0e3 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 8 Dec 2019 00:22:30 +0200 Subject: [PATCH 17/21] respect TargetType in controltemplate --- ...valoniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From 88ca29cdf2d750aecf6a96b9e3e5b2f49ca48f21 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 8 Dec 2019 00:38:51 +0200 Subject: [PATCH 18/21] fixed issue #3145 in xamlil --- src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index ad9915e193..dadd6e1358 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 dadd6e13581eff304ccda6bbc9f7685338c580c6 From 59c46572f6fb4921f064fc21d12bacba750c41cc Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 8 Dec 2019 10:48:33 +0200 Subject: [PATCH 19/21] update submodule back to main xamlil repo --- src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index dadd6e1358..4c4b6cf8ff 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit dadd6e13581eff304ccda6bbc9f7685338c580c6 +Subproject commit 4c4b6cf8ff0894c925d87b27d4fc7a064440c218 From 1f2b2e965279fb8cdcb57712780f7a6c1d1f90a4 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 9 Dec 2019 18:15:39 +0200 Subject: [PATCH 20/21] document transform.parse --- src/Avalonia.Visuals/Media/Transform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs index 30a07d47ef..8253d11ff1 100644 --- a/src/Avalonia.Visuals/Media/Transform.cs +++ b/src/Avalonia.Visuals/Media/Transform.cs @@ -31,7 +31,7 @@ namespace Avalonia.Media /// /// Parses a string. /// - /// The string. + /// Six comma-delimited double values that describe the new . For details check /// The . public static Transform Parse(string s) { From 78b790e6ebb400efb79a2988052a7b340e51c5dc Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 9 Dec 2019 18:17:47 +0200 Subject: [PATCH 21/21] document matrix.parse --- src/Avalonia.Visuals/Matrix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) {