diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf new file mode 100644 index 0000000000..f6986468bd Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Bold.ttf differ diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf new file mode 100644 index 0000000000..5c00b64faf Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-BoldItalic.ttf differ diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf new file mode 100644 index 0000000000..82e8762011 Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Italic.ttf differ diff --git a/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000000..278ad8aa0a Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/SourceSansPro-Regular.ttf differ diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index b8a8479a49..c37002ef6a 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -11,6 +11,12 @@ + + + + + + diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index db1c9cb69d..fb0d8fb7b1 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -31,6 +31,12 @@ - + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IAssetLoader.cs b/src/Avalonia.Base/Platform/IAssetLoader.cs index ba30af60bf..dda2cbc2d5 100644 --- a/src/Avalonia.Base/Platform/IAssetLoader.cs +++ b/src/Avalonia.Base/Platform/IAssetLoader.cs @@ -2,6 +2,7 @@ // 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.IO; using System.Reflection; @@ -18,8 +19,8 @@ namespace Avalonia.Platform /// AssetLoader needs a refactor cause right now it lives in 3+ platforms which /// can all be loaded on Windows. /// - /// - void SetDefaultAssembly(Assembly asm); + /// + void SetDefaultAssembly(Assembly assembly); /// /// Checks if an asset with the specified URI exists. @@ -32,32 +33,39 @@ namespace Avalonia.Platform bool Exists(Uri uri, Uri baseUri = null); /// - /// Opens the resource with the requested URI. + /// Opens the asset with the requested URI. /// /// The URI. /// /// A base URI to use if is relative. /// - /// A stream containing the resource contents. + /// A stream containing the asset contents. /// - /// The resource was not found. + /// The asset could not be found. /// Stream Open(Uri uri, Uri baseUri = null); /// - /// Opens the resource with the requested URI and returns the resource string and the - /// assembly containing the resource. + /// Opens the asset with the requested URI and returns the asset stream and the + /// assembly containing the asset. /// /// The URI. /// /// A base URI to use if is relative. /// /// - /// The stream containing the resource contents together with the assembly. + /// The stream containing the asset contents together with the assembly. /// /// - /// The resource was not found. + /// The asset could not be found. /// - Tuple OpenAndGetAssembly(Uri uri, Uri baseUri = null); + (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri baseUri = null); + + /// + /// Gets all assets of a folder and subfolders that match specified uri. + /// + /// The URI. + /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset + IEnumerable<(string absolutePath, Assembly assembly)> GetAssets(Uri uri); } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 6a26e29187..5119096965 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -364,6 +364,7 @@ namespace Avalonia.Controls var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); + PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); } /// @@ -421,6 +422,7 @@ namespace Avalonia.Controls private void SubscribeToItems(IEnumerable items) { PseudoClasses.Set(":empty", items == null || items.Count() == 0); + PseudoClasses.Set(":singleitem", items != null && items.Count() == 1); var incc = items as INotifyCollectionChanged; diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 1a805a3822..8514104c91 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty FontFamilyProperty = + public static readonly StyledProperty FontFamilyProperty = TextBlock.FontFamilyProperty.AddOwner(); /// @@ -141,7 +141,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the font family used to draw the control's text. /// - public string FontFamily + public FontFamily FontFamily { get { return GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index e7cf666589..fd46efa76f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -4,7 +4,6 @@ using System; using System.Reactive; using System.Reactive.Linq; -using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; @@ -28,10 +27,10 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly AttachedProperty FontFamilyProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty FontFamilyProperty = + AvaloniaProperty.RegisterAttached( nameof(FontFamily), - defaultValue: "Courier New", + defaultValue: FontFamily.Default, inherits: true); /// @@ -148,7 +147,7 @@ namespace Avalonia.Controls /// /// Gets or sets the font family. /// - public string FontFamily + public FontFamily FontFamily { get { return GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } @@ -229,7 +228,7 @@ namespace Avalonia.Controls /// /// The control. /// The font family. - public static string GetFontFamily(Control control) + public static FontFamily GetFontFamily(Control control) { return control.GetValue(FontFamilyProperty); } @@ -280,7 +279,7 @@ namespace Avalonia.Controls /// The control. /// The property value to set. /// The font family. - public static void SetFontFamily(Control control, string value) + public static void SetFontFamily(Control control, FontFamily value) { control.SetValue(FontFamilyProperty, value); } diff --git a/src/Avalonia.Themes.Default/RadioButton.xaml b/src/Avalonia.Themes.Default/RadioButton.xaml index fb71432595..d29cb89452 100644 --- a/src/Avalonia.Themes.Default/RadioButton.xaml +++ b/src/Avalonia.Themes.Default/RadioButton.xaml @@ -8,7 +8,7 @@ diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs new file mode 100644 index 0000000000..a4b4f5085a --- /dev/null +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -0,0 +1,179 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Media.Fonts; + +namespace Avalonia.Media +{ + public class FontFamily + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the . + /// name + public FontFamily(string name) + { + Contract.Requires(name != null); + + FamilyNames = new FamilyNameCollection(new[] { name }); + } + + /// + /// Initializes a new instance of the class. + /// + /// The names of the . + /// name + public FontFamily(IEnumerable names) + { + Contract.Requires(names != null); + + FamilyNames = new FamilyNameCollection(names); + } + + /// + /// + /// Initializes a new instance of the class. + /// + /// The name of the . + /// The source of font resources. + public FontFamily(string name, Uri source) : this(name) + { + Key = new FontFamilyKey(source); + } + + /// + /// Represents the default font family + /// + public static FontFamily Default => new FontFamily("Courier New"); + + /// + /// Gets the primary family name of the font family. + /// + /// + /// The primary name of the font family. + /// + public string Name => FamilyNames.PrimaryFamilyName; + + /// + /// Gets the family names. + /// + /// + /// The family familyNames. + /// + public FamilyNameCollection FamilyNames { get; } + + /// + /// Gets the key for associated assets. + /// + /// + /// The family familyNames. + /// + public FontFamilyKey Key { get; } + + /// + /// Implicit conversion of string to FontFamily + /// + /// + public static implicit operator FontFamily(string fontFamily) + { + return new FontFamily(fontFamily); + } + + /// + /// Parses a string. + /// + /// The string. + /// + /// + /// Specified family is not supported. + /// + public static FontFamily Parse(string s) + { + if (string.IsNullOrEmpty(s)) + { + throw new ArgumentException("Specified family is not supported."); + } + + var segments = s.Split('#'); + + switch (segments.Length) + { + case 1: + { + var names = segments[0].Split(',') + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)); + return new FontFamily(names); + } + + case 2: + { + return new FontFamily(segments[1], new Uri(segments[0], UriKind.RelativeOrAbsolute)); + } + + default: + { + throw new ArgumentException("Specified family is not supported."); + } + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (Key != null) + { + return Key + "#" + FamilyNames; + } + + return FamilyNames.ToString(); + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() + { + unchecked + { + var hash = (int)2186146271; + + hash = (hash * 15768619) ^ FamilyNames.GetHashCode(); + + if (Key != null) + { + hash = (hash * 15768619) ^ Key.GetHashCode(); + } + + return hash; + } + } + + public override bool Equals(object obj) + { + if (!(obj is FontFamily other)) + { + return false; + } + + if (Key != null) + { + return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key); + } + + return other.FamilyNames.Equals(FamilyNames); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs new file mode 100644 index 0000000000..50511d2fb7 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -0,0 +1,131 @@ +// 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 System.Collections.ObjectModel; +using System.Linq; + +namespace Avalonia.Media.Fonts +{ + using System.Text; + + public class FamilyNameCollection : IEnumerable + { + /// + /// Initializes a new instance of the class. + /// + /// The family names. + /// familyNames + public FamilyNameCollection(IEnumerable familyNames) + { + Contract.Requires(familyNames != null); + + var names = new List(familyNames); + + if (names.Count == 0) throw new ArgumentException($"{nameof(familyNames)} must not be empty."); + + Names = new ReadOnlyCollection(names); + + PrimaryFamilyName = Names.First(); + + HasFallbacks = Names.Count > 1; + } + + /// + /// Gets the primary family name. + /// + /// + /// The primary family name. + /// + public string PrimaryFamilyName { get; } + + /// + /// Gets a value indicating whether fallbacks are defined. + /// + /// + /// true if fallbacks are defined; otherwise, false. + /// + public bool HasFallbacks { get; } + + /// + /// Gets the internal collection of names. + /// + /// + /// The names. + /// + internal ReadOnlyCollection Names { get; } + + /// + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return Names.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(); + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + var builder = new StringBuilder(); + + for (var index = 0; index < Names.Count; index++) + { + builder.Append(Names[index]); + + if (index == Names.Count - 1) break; + + builder.Append(", "); + } + + return builder.ToString(); + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object obj) + { + if (!(obj is FamilyNameCollection other)) return false; + + return other.ToString().Equals(ToString()); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs new file mode 100644 index 0000000000..c2005c9acb --- /dev/null +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs @@ -0,0 +1,106 @@ +// 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.Linq; + +namespace Avalonia.Media.Fonts +{ + /// + /// Represents an identifier for a + /// + public class FontFamilyKey + { + /// + /// Creates a new instance of and extracts and from given + /// + /// + public FontFamilyKey(Uri source) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + + if (source.AbsolutePath.Contains(".ttf")) + { + var filePathWithoutExtension = source.AbsolutePath.Replace(".ttf", string.Empty); + var fileNameWithoutExtension = filePathWithoutExtension.Split('.').Last(); + FileName = fileNameWithoutExtension + ".ttf"; + Location = new Uri(source.OriginalString.Replace("." + FileName, string.Empty), UriKind.RelativeOrAbsolute); + } + else + { + Location = source; + } + } + + /// + /// Location of stored font asset that belongs to a + /// + public Uri Location { get; } + + /// + /// Optional filename for a font asset that belongs to a + /// + public string FileName { get; } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() + { + unchecked + { + var hash = (int)2166136261; + + if (Location != null) + { + hash = (hash * 16777619) ^ Location.GetHashCode(); + } + + if (FileName != null) + { + hash = (hash * 16777619) ^ FileName.GetHashCode(); + } + + return hash; + } + } + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object obj) + { + if (!(obj is FontFamilyKey other)) return false; + + if (Location != other.Location) return false; + + if (FileName != other.FileName) return false; + + return true; + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (FileName == null) return Location.PathAndQuery; + + var builder = new UriBuilder(Location); + + builder.Path += "." + FileName; + + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs new file mode 100644 index 0000000000..2b77371bb5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs @@ -0,0 +1,72 @@ +// 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.Linq; +using System.Reflection; +using Avalonia.Platform; + +namespace Avalonia.Media.Fonts +{ + public static class FontFamilyLoader + { + private static readonly IAssetLoader s_assetLoader; + + static FontFamilyLoader() + { + s_assetLoader = AvaloniaLocator.Current.GetService(); + } + + public static IEnumerable LoadFontAssets(FontFamilyKey fontFamilyKey) + { + return fontFamilyKey.FileName != null + ? GetFontAssetsByFileName(fontFamilyKey.Location, fontFamilyKey.FileName) + : GetFontAssetsByLocation(fontFamilyKey.Location); + } + + /// + /// Searches for font assets at a given location and returns a quanity of found assets + /// + /// + /// + private static IEnumerable GetFontAssetsByLocation(Uri location) + { + var availableAssets = s_assetLoader.GetAssets(location); + + var matchingAssets = availableAssets.Where(x => x.absolutePath.EndsWith(".ttf")); + + return matchingAssets.Select(x => GetAssetUri(x.absolutePath, x.assembly)); + } + + /// + /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression. + /// File names can target multiple files with * wildcard. For example "FontFile*.ttf" + /// + /// + /// + /// + private static IEnumerable GetFontAssetsByFileName(Uri location, string fileName) + { + var availableResources = s_assetLoader.GetAssets(location); + + var compareTo = location.AbsolutePath + "." + fileName.Split('*').First(); + + var matchingResources = + availableResources.Where(x => x.absolutePath.Contains(compareTo) && x.absolutePath.EndsWith(".ttf")); + + return matchingResources.Select(x => GetAssetUri(x.absolutePath, x.assembly)); + } + + /// + /// Returns a for a font asset that follows the resm scheme + /// + /// + /// + /// + private static Uri GetAssetUri(string absolutePath, Assembly assembly) + { + return new Uri("resm:" + absolutePath + "?assembly=" + assembly.GetName().Name, UriKind.RelativeOrAbsolute); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 12540b67e7..40e98d1565 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -10,13 +10,13 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// - /// The name of the font family. + /// The font family. /// The font size, in DIPs. /// The font style. /// The font weight. public Typeface( - string fontFamilyName, - double fontSize, + FontFamily fontFamily, + double fontSize = 12, FontStyle style = FontStyle.Normal, FontWeight weight = FontWeight.Normal) { @@ -30,16 +30,32 @@ namespace Avalonia.Media throw new ArgumentException("Font weight must be > 0."); } - FontFamilyName = fontFamilyName; + FontFamily = fontFamily; FontSize = fontSize; Style = style; Weight = weight; } /// - /// Gets the name of the font family. + /// Initializes a new instance of the class. + /// + /// The name of the font family. + /// The font size, in DIPs. + /// The font style. + /// The font weight. + public Typeface( + string fontFamilyName, + double fontSize = 12, + FontStyle style = FontStyle.Normal, + FontWeight weight = FontWeight.Normal) + : this(new FontFamily(fontFamilyName), fontSize, style, weight) + { + } + + /// + /// Gets the font family. /// - public string FontFamilyName { get; } + public FontFamily FontFamily { get; } /// /// Gets the size of the font in DIPs. diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index eac362e997..ed464ec7f9 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering { _fpsText = new FormattedText { - Typeface = new Typeface(null, 18), + Typeface = s_fpsTypeface }; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 8bfe8c25bd..9f51af730f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -28,6 +28,10 @@ + + + + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 79523dd498..f3fac3faf7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -126,11 +126,11 @@ namespace Avalonia.Markup.Xaml } var asset = assetLocator.OpenAndGetAssembly(uri, baseUri); - using (var stream = asset.Item1) + using (var stream = asset.stream) { try { - return Load(stream, asset.Item2, rootInstance, uri); + return Load(stream, asset.assembly, rootInstance, uri); } catch (Exception e) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs new file mode 100644 index 0000000000..9d03db5fa8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/FontFamilyTypeConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using Avalonia.Media; + +namespace Avalonia.Markup.Xaml.Converters +{ + public class FontFamilyTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return FontFamily.Parse((string)value); + } + } +} \ No newline at end of file diff --git a/src/Shared/PlatformSupport/AssetLoader.cs b/src/Shared/PlatformSupport/AssetLoader.cs index fa11edb57b..b4d17b22a1 100644 --- a/src/Shared/PlatformSupport/AssetLoader.cs +++ b/src/Shared/PlatformSupport/AssetLoader.cs @@ -57,21 +57,21 @@ namespace Avalonia.Shared.PlatformSupport } /// - /// Opens the resource with the requested URI. + /// Opens the asset with the requested URI. /// /// The URI. /// /// A base URI to use if is relative. /// - /// A stream containing the resource contents. + /// A stream containing the asset contents. /// - /// The resource was not found. + /// The asset could not be found. /// public Stream Open(Uri uri, Uri baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; - + /// - /// Opens the resource with the requested URI and returns the resource string and the - /// assembly containing the resource. + /// Opens the asset with the requested URI and returns the asset stream and the + /// assembly containing the asset. /// /// The URI. /// @@ -81,9 +81,9 @@ namespace Avalonia.Shared.PlatformSupport /// The stream containing the resource contents together with the assembly. /// /// - /// The resource was not found. + /// The asset could not be found. /// - public Tuple OpenAndGetAssembly(Uri uri, Uri baseUri = null) + public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri baseUri = null) { var asset = GetAsset(uri, baseUri); @@ -92,7 +92,21 @@ namespace Avalonia.Shared.PlatformSupport throw new FileNotFoundException($"The resource {uri} could not be found."); } - return Tuple.Create(asset.GetStream(), asset.Assembly); + return (asset.GetStream(), asset.Assembly); + } + + /// + /// Gets all assets of a folder and subfolders that match specified uri. + /// + /// The URI. + /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset + public IEnumerable<(string absolutePath, Assembly assembly)> GetAssets(Uri uri) + { + var assembly = GetAssembly(uri); + + return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath)) + .Select(x => (x.Key, x.Value.Assembly)) ?? + Enumerable.Empty<(string AbsolutePath, Assembly Assembly)>(); } private IAssetDescriptor GetAsset(Uri uri, Uri baseUri) @@ -219,4 +233,4 @@ namespace Avalonia.Shared.PlatformSupport public string Name { get; } } } -} +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index f22722a0b5..00f0a48a7b 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -25,10 +25,34 @@ namespace Avalonia.Skia // Replace 0 characters with zero-width spaces (200B) Text = Text.Replace((char)0, (char)0x200B); - var skiaTypeface = TypefaceCache.GetTypeface( - typeface?.FontFamilyName ?? "monospace", - typeface?.Style ?? FontStyle.Normal, - typeface?.Weight ?? FontWeight.Normal); + SKTypeface skiaTypeface = TypefaceCache.Default; + + if (typeface.FontFamily.Key != null) + { + var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); + skiaTypeface = typefaces.GetTypeFace(typeface); + } + else + { + if (typeface.FontFamily.FamilyNames.HasFallbacks) + { + foreach (var familyName in typeface.FontFamily.FamilyNames) + { + skiaTypeface = TypefaceCache.GetTypeface( + familyName, + typeface.Style, + typeface.Weight); + if (skiaTypeface != TypefaceCache.Default) break; + } + } + else + { + skiaTypeface = TypefaceCache.GetTypeface( + typeface.FontFamily.Name, + typeface.Style, + typeface.Weight); + } + } _paint = new SKPaint(); @@ -36,11 +60,11 @@ namespace Avalonia.Skia //Paint.TextEncoding = SKTextEncoding.Utf8; _paint.TextEncoding = SKTextEncoding.Utf16; _paint.IsStroke = false; - _paint.IsAntialias = true; - _paint.LcdRenderText = true; + _paint.IsAntialias = true; + _paint.LcdRenderText = true; _paint.SubpixelText = true; _paint.Typeface = skiaTypeface; - _paint.TextSize = (float)(typeface?.FontSize ?? 12); + _paint.TextSize = (float)typeface.FontSize; _paint.TextAlign = textAlignment.ToSKTextAlign(); _wrapping = wrapping; @@ -246,7 +270,7 @@ namespace Avalonia.Skia subStr = Text.Substring(i, len); ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); - + canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); i += len; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs new file mode 100644 index 0000000000..bd951ef3a3 --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -0,0 +1,74 @@ +using System.Collections.Concurrent; +using Avalonia.Media; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal class SKTypefaceCollection + { + private readonly ConcurrentDictionary _cachedTypefaces = + new ConcurrentDictionary(); + + public void AddTypeFace(SKTypeface typeface) + { + var key = new FontKey(typeface.FamilyName, (SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant); + + _cachedTypefaces.TryAdd(key, typeface); + } + + public SKTypeface GetTypeFace(Typeface typeface) + { + SKFontStyleSlant skStyle = SKFontStyleSlant.Upright; + + switch (typeface.Style) + { + case FontStyle.Italic: + skStyle = SKFontStyleSlant.Italic; + break; + + case FontStyle.Oblique: + skStyle = SKFontStyleSlant.Oblique; + break; + } + + var key = new FontKey(typeface.FontFamily.Name, (SKFontStyleWeight)typeface.Weight, skStyle); + + return _cachedTypefaces.TryGetValue(key, out var skTypeface) ? skTypeface : TypefaceCache.Default; + } + + private struct FontKey + { + public readonly string Name; + public readonly SKFontStyleSlant Slant; + public readonly SKFontStyleWeight Weight; + + public FontKey(string name, SKFontStyleWeight weight, SKFontStyleSlant slant) + { + Name = name; + Slant = slant; + Weight = weight; + } + + public override int GetHashCode() + { + int hash = 17; + hash = hash * 31 + Name.GetHashCode(); + hash = hash * 31 + (int)Slant; + hash = hash * 31 + (int)Weight; + + return hash; + } + + public override bool Equals(object other) + { + return other is FontKey ? Equals((FontKey)other) : false; + } + + private bool Equals(FontKey other) + { + return Name == other.Name && Slant == other.Slant && + Weight == other.Weight; + } + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs new file mode 100644 index 0000000000..dc533cbf3c --- /dev/null +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -0,0 +1,53 @@ +using System.Collections.Concurrent; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + internal static class SKTypefaceCollectionCache + { + private static readonly ConcurrentDictionary s_cachedCollections; + + static SKTypefaceCollectionCache() + { + s_cachedCollections = new ConcurrentDictionary(); + } + + /// + /// Gets the or add typeface collection. + /// + /// The font family. + /// + public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) + { + return s_cachedCollections.GetOrAdd(fontFamily.Key, x => CreateCustomFontCollection(fontFamily)); + } + + /// + /// Creates the custom font collection. + /// + /// The font family. + /// + private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) + { + var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key); + + var typeFaceCollection = new SKTypefaceCollection(); + + var assetLoader = AvaloniaLocator.Current.GetService(); + + foreach (var asset in fontAssets) + { + var assetStream = assetLoader.Open(asset); + + var typeface = SKTypeface.FromStream(assetStream); + + typeFaceCollection.AddTypeFace(typeface); + } + + return typeFaceCollection; + } + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs index c5f3d371ad..24674f3b22 100644 --- a/src/Skia/Avalonia.Skia/TypefaceCache.cs +++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs @@ -1,6 +1,5 @@ -using System; using System.Collections.Generic; -using System.Text; +using System.Linq; using Avalonia.Media; using SkiaSharp; @@ -8,6 +7,7 @@ namespace Avalonia.Skia { static class TypefaceCache { + public static SKTypeface Default = SKTypeface.FromFamilyName(FontFamily.Default.Name); static readonly Dictionary> Cache = new Dictionary>(); struct FontKey @@ -15,9 +15,9 @@ namespace Avalonia.Skia public readonly SKFontStyleSlant Slant; public readonly SKFontStyleWeight Weight; - public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) + public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant) { - Slant = slant; + Slant = slant; Weight = weight; } @@ -38,35 +38,28 @@ namespace Avalonia.Skia public bool Equals(FontKey other) { return Slant == other.Slant && - Weight == other.Weight; + Weight == other.Weight; } // Equals and GetHashCode ommitted } - unsafe static SKTypeface GetTypeface(string name, FontKey key) + private static SKTypeface GetTypeface(string name, FontKey key) { - if (name == null) - { - name = "Arial"; - } + var familyKey = name; - Dictionary entry; - - if (!Cache.TryGetValue(name, out entry)) + if (!Cache.TryGetValue(familyKey, out var entry)) { - Cache[name] = entry = new Dictionary(); + Cache[familyKey] = entry = new Dictionary(); } - SKTypeface typeface = null; - - if (!entry.TryGetValue(key, out typeface)) + if (!entry.TryGetValue(key, out var typeface)) { - typeface = SKTypeface.FromFamilyName(name, key.Weight, SKFontStyleWidth.Normal, key.Slant); + typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant); - if (typeface == null) + if (typeface.FamilyName != name) { - typeface = SKTypeface.FromFamilyName(null, SKTypefaceStyle.Normal); + typeface = Default; } entry[key] = typeface; @@ -79,7 +72,7 @@ namespace Avalonia.Skia { SKFontStyleSlant skStyle = SKFontStyleSlant.Upright; - switch(style) + switch (style) { case FontStyle.Italic: skStyle = SKFontStyleSlant.Italic; diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileEnumerator.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileEnumerator.cs new file mode 100644 index 0000000000..c144e12aea --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileEnumerator.cs @@ -0,0 +1,67 @@ +using SharpDX; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// Resource FontFileEnumerator. + /// + public class DWriteResourceFontFileEnumerator : CallbackBase, FontFileEnumerator + { + private readonly Factory _factory; + private readonly FontFileLoader _loader; + private readonly DataStream _keyStream; + private FontFile _currentFontFile; + + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// The loader. + /// The key. + public DWriteResourceFontFileEnumerator(Factory factory, FontFileLoader loader, DataPointer key) + { + _factory = factory; + _loader = loader; + _keyStream = new DataStream(key.Pointer, key.Size, true, false); + } + + /// + /// Advances to the next font file in the collection. When it is first created, the enumerator is positioned before the first element of the collection and the first call to MoveNext advances to the first file. + /// + /// + /// the value TRUE if the enumerator advances to a file; otherwise, FALSE if the enumerator advances past the last file in the collection. + /// + /// HRESULT IDWriteFontFileEnumerator::MoveNext([Out] BOOL* hasCurrentFile) + bool FontFileEnumerator.MoveNext() + { + bool moveNext = _keyStream.RemainingLength != 0; + + if (!moveNext) return false; + + _currentFontFile?.Dispose(); + + _currentFontFile = new FontFile(_factory, _keyStream.PositionPointer, 4, _loader); + + _keyStream.Position += 4; + + return true; + } + + /// + /// Gets a reference to the current font file. + /// + /// + /// a reference to the newly created object. + /// HRESULT IDWriteFontFileEnumerator::GetCurrentFontFile([Out] IDWriteFontFile** fontFile) + FontFile FontFileEnumerator.CurrentFontFile + { + get + { + ((IUnknown)_currentFontFile).AddReference(); + + return _currentFontFile; + } + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileStream.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileStream.cs new file mode 100644 index 0000000000..1b1cb85495 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontFileStream.cs @@ -0,0 +1,84 @@ +using System; +using SharpDX; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// This FontFileStream implem is reading data from a . + /// + public class DWriteResourceFontFileStream : CallbackBase, FontFileStream + { + private readonly DataStream _stream; + + /// + /// Initializes a new instance of the class. + /// + /// The stream. + public DWriteResourceFontFileStream(DataStream stream) + { + _stream = stream; + } + + /// + /// Reads a fragment from a font file. + /// + /// When this method returns, contains an address of a reference to the start of the font file fragment. This parameter is passed uninitialized. + /// The offset of the fragment, in bytes, from the beginning of the font file. + /// The size of the file fragment, in bytes. + /// When this method returns, contains the address of + /// + /// Note that ReadFileFragment implementations must check whether the requested font file fragment is within the file bounds. Otherwise, an error should be returned from ReadFileFragment. {{DirectWrite}} may invoke methods on the same object from multiple threads simultaneously. Therefore, ReadFileFragment implementations that rely on internal mutable state must serialize access to such state across multiple threads. For example, an implementation that uses separate Seek and Read operations to read a file fragment must place the code block containing Seek and Read calls under a lock or a critical section. + /// + /// HRESULT IDWriteFontFileStream::ReadFileFragment([Out, Buffer] const void** fragmentStart,[None] __int64 fileOffset,[None] __int64 fragmentSize,[Out] void** fragmentContext) + void FontFileStream.ReadFileFragment(out IntPtr fragmentStart, long fileOffset, long fragmentSize, out IntPtr fragmentContext) + { + lock (this) + { + fragmentContext = IntPtr.Zero; + + _stream.Position = fileOffset; + + fragmentStart = _stream.PositionPointer; + } + } + + /// + /// Releases a fragment from a file. + /// + /// A reference to the client-defined context of a font fragment returned from {{ReadFileFragment}}. + /// void IDWriteFontFileStream::ReleaseFileFragment([None] void* fragmentContext) + void FontFileStream.ReleaseFileFragment(IntPtr fragmentContext) + { + // Nothing to release. No context are used + } + + /// + /// Obtains the total size of a file. + /// + /// the total size of the file. + /// + /// Implementing GetFileSize() for asynchronously loaded font files may require downloading the complete file contents. Therefore, this method should be used only for operations that either require a complete font file to be loaded (for example, copying a font file) or that need to make decisions based on the value of the file size (for example, validation against a persisted file size). + /// + /// HRESULT IDWriteFontFileStream::GetFileSize([Out] __int64* fileSize) + long FontFileStream.GetFileSize() + { + return _stream.Length; + } + + /// + /// Obtains the last modified time of the file. + /// + /// + /// the last modified time of the file in the format that represents the number of 100-nanosecond intervals since January 1, 1601 (UTC). + /// + /// + /// The "last modified time" is used by DirectWrite font selection algorithms to determine whether one font resource is more up to date than another one. + /// + /// HRESULT IDWriteFontFileStream::GetLastWriteTime([Out] __int64* lastWriteTime) + long FontFileStream.GetLastWriteTime() + { + return 0; + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs new file mode 100644 index 0000000000..065da8d3e8 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/DWriteResourceFontLoader.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using Avalonia.Media.Fonts; +using Avalonia.Platform; +using SharpDX; +using SharpDX.DirectWrite; + +namespace Avalonia.Direct2D1.Media +{ + using System; + + internal class DWriteResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader + { + private readonly List _fontStreams = new List(); + private readonly List _enumerators = new List(); + private readonly DataStream _keyStream; + + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// + public DWriteResourceFontLoader(Factory factory, IEnumerable fontAssets) + { + var factory1 = factory; + + var assetLoader = AvaloniaLocator.Current.GetService(); + + foreach (var asset in fontAssets) + { + var assetStream = assetLoader.Open(asset); + + var dataStream = new DataStream((int)assetStream.Length, true, true); + + assetStream.CopyTo(dataStream); + + dataStream.Position = 0; + + _fontStreams.Add(new DWriteResourceFontFileStream(dataStream)); + } + + // Build a Key storage that stores the index of the font + _keyStream = new DataStream(sizeof(int) * _fontStreams.Count, true, true); + + for (int i = 0; i < _fontStreams.Count; i++) + { + _keyStream.Write(i); + } + + _keyStream.Position = 0; + + // Register the + factory1.RegisterFontFileLoader(this); + factory1.RegisterFontCollectionLoader(this); + } + + + /// + /// Gets the key used to identify the FontCollection as well as storing index for fonts. + /// + /// The key. + public DataStream Key => _keyStream; + + /// + /// Creates a font file enumerator object that encapsulates a collection of font files. The font system calls back to this interface to create a font collection. + /// + /// Pointer to the object that was used to create the current font collection. + /// A font collection key that uniquely identifies the collection of font files within the scope of the font collection loader being used. The buffer allocated for this key must be at least the size, in bytes, specified by collectionKeySize. + /// + /// a reference to the newly created font file enumerator. + /// + /// HRESULT IDWriteFontCollectionLoader::CreateEnumeratorFromKey([None] IDWriteFactory* factory,[In, Buffer] const void* collectionKey,[None] int collectionKeySize,[Out] IDWriteFontFileEnumerator** fontFileEnumerator) + FontFileEnumerator FontCollectionLoader.CreateEnumeratorFromKey(Factory factory, DataPointer collectionKey) + { + var enumerator = new DWriteResourceFontFileEnumerator(factory, this, collectionKey); + + _enumerators.Add(enumerator); + + return enumerator; + } + + /// + /// Creates a font file stream object that encapsulates an open file resource. + /// + /// A reference to a font file reference key that uniquely identifies the font file resource within the scope of the font loader being used. The buffer allocated for this key must at least be the size, in bytes, specified by fontFileReferenceKeySize. + /// + /// a reference to the newly created object. + /// + /// + /// The resource is closed when the last reference to fontFileStream is released. + /// + /// HRESULT IDWriteFontFileLoader::CreateStreamFromKey([In, Buffer] const void* fontFileReferenceKey,[None] int fontFileReferenceKeySize,[Out] IDWriteFontFileStream** fontFileStream) + FontFileStream FontFileLoader.CreateStreamFromKey(DataPointer fontFileReferenceKey) + { + var index = SharpDX.Utilities.Read(fontFileReferenceKey.Pointer); + + return _fontStreams[index]; + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs new file mode 100644 index 0000000000..4199c73c54 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs @@ -0,0 +1,65 @@ +using System.Collections.Concurrent; +using Avalonia.Media; +using Avalonia.Media.Fonts; + +namespace Avalonia.Direct2D1.Media +{ + internal static class Direct2D1FontCollectionCache + { + private static readonly ConcurrentDictionary s_cachedCollections; + private static readonly SharpDX.DirectWrite.Factory s_factory; + private static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection; + + static Direct2D1FontCollectionCache() + { + s_cachedCollections = new ConcurrentDictionary(); + + s_factory = AvaloniaLocator.Current.GetService(); + + s_installedFontCollection = s_factory.GetSystemFontCollection(false); + } + + public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface) + { + var fontFamily = typeface.FontFamily; + var fontCollection = GetOrAddFontCollection(fontFamily); + var fontFamilyName = FontFamily.Default.Name; + + // Should this be cached? + foreach (var familyName in fontFamily.FamilyNames) + { + if (!fontCollection.FindFamilyName(familyName, out _)) + { + continue; + } + + fontFamilyName = familyName; + + break; + } + + return new SharpDX.DirectWrite.TextFormat( + s_factory, + fontFamilyName, + fontCollection, + (SharpDX.DirectWrite.FontWeight)typeface.Weight, + (SharpDX.DirectWrite.FontStyle)typeface.Style, + SharpDX.DirectWrite.FontStretch.Normal, + (float)typeface.FontSize); + } + + private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily) + { + return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection); + } + + private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key) + { + var assets = FontFamilyLoader.LoadFontAssets(key); + + var fontLoader = new DWriteResourceFontLoader(s_factory, assets); + + return new SharpDX.DirectWrite.FontCollection(s_factory, fontLoader, fontLoader.Key); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index 5578abc32c..0f09a1827b 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -1,7 +1,6 @@ // 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.Linq; using Avalonia.Media; @@ -21,29 +20,25 @@ namespace Avalonia.Direct2D1.Media IReadOnlyList spans) { Text = text; + var factory = AvaloniaLocator.Current.GetService(); - using (var format = new DWrite.TextFormat( - factory, - typeface?.FontFamilyName ?? "Courier New", - (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal), - (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal), - (float)(typeface?.FontSize ?? 12))) + var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface); + + textFormat.WordWrapping = + wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap; + + TextLayout = new DWrite.TextLayout( + factory, + Text ?? string.Empty, + textFormat, + (float)constraint.Width, + (float)constraint.Height) { - format.WordWrapping = wrapping == TextWrapping.Wrap ? - DWrite.WordWrapping.Wrap : - DWrite.WordWrapping.NoWrap; - - TextLayout = new DWrite.TextLayout( - factory, - text ?? string.Empty, - format, - (float)constraint.Width, - (float)constraint.Height) - { - TextAlignment = textAlignment.ToDirect2D() - }; - } + TextAlignment = textAlignment.ToDirect2D() + }; + + textFormat.Dispose(); if (spans != null) { @@ -64,11 +59,6 @@ namespace Avalonia.Direct2D1.Media public DWrite.TextLayout TextLayout { get; } - public void Dispose() - { - TextLayout.Dispose(); - } - public IEnumerable GetLines() { var result = TextLayout.GetLineMetrics(); @@ -77,14 +67,11 @@ namespace Avalonia.Direct2D1.Media public TextHitTestResult HitTestPoint(Point point) { - SharpDX.Mathematics.Interop.RawBool isTrailingHit; - SharpDX.Mathematics.Interop.RawBool isInside; - var result = TextLayout.HitTestPoint( (float)point.X, (float)point.Y, - out isTrailingHit, - out isInside); + out var isTrailingHit, + out var isInside); return new TextHitTestResult { @@ -96,14 +83,7 @@ namespace Avalonia.Direct2D1.Media public Rect HitTestTextPosition(int index) { - float x; - float y; - - var result = TextLayout.HitTestTextPosition( - index, - false, - out x, - out y); + var result = TextLayout.HitTestTextPosition(index, false, out _, out _); return new Rect(result.Left, result.Top, result.Width, result.Height); } diff --git a/tests/Avalonia.RenderTests/Controls/BorderTests.cs b/tests/Avalonia.RenderTests/Controls/BorderTests.cs index 7d2e40c3b4..c82d616094 100644 --- a/tests/Avalonia.RenderTests/Controls/BorderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/BorderTests.cs @@ -204,7 +204,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, HorizontalAlignment = HorizontalAlignment.Center, } @@ -231,7 +231,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, VerticalAlignment = VerticalAlignment.Center, } @@ -258,7 +258,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, HorizontalAlignment = HorizontalAlignment.Stretch, } @@ -285,7 +285,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, VerticalAlignment = VerticalAlignment.Stretch, } @@ -312,7 +312,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, HorizontalAlignment = HorizontalAlignment.Left, } @@ -339,7 +339,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, HorizontalAlignment = HorizontalAlignment.Right, } @@ -366,7 +366,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, VerticalAlignment = VerticalAlignment.Top, } @@ -393,7 +393,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { Text = "Foo", Background = Brushes.Red, - FontFamily = "Segoe UI", + FontFamily = new FontFamily("Segoe UI"), FontSize = 12, VerticalAlignment = VerticalAlignment.Bottom, } diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index cfa15ae304..f69d336271 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -48,7 +48,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media Child = new TextBlock { FontSize = 24, - FontFamily = "Arial", + FontFamily = new FontFamily("Arial"), Background = Brushes.Green, Foreground = Brushes.Yellow, Text = "VisualBrush", diff --git a/tests/Avalonia.UnitTests/MockAssetLoader.cs b/tests/Avalonia.UnitTests/MockAssetLoader.cs index d6b70aee16..11d66128a7 100644 --- a/tests/Avalonia.UnitTests/MockAssetLoader.cs +++ b/tests/Avalonia.UnitTests/MockAssetLoader.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; -using System.Threading.Tasks; using Avalonia.Platform; namespace Avalonia.UnitTests @@ -27,10 +26,16 @@ namespace Avalonia.UnitTests { return new MemoryStream(Encoding.UTF8.GetBytes(_assets[uri])); } - - public Tuple OpenAndGetAssembly(Uri uri, Uri baseUri = null) + + public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri baseUri = null) + { + return (Open(uri, baseUri), (Assembly)null); + } + + public IEnumerable<(string absolutePath, Assembly assembly)> GetAssets(Uri uri) { - return Tuple.Create(Open(uri, baseUri), (Assembly)null); + return _assets.Keys.Where(x => x.AbsolutePath.Contains(uri.AbsolutePath)) + .Select(x => (x.AbsolutePath, Assembly.GetEntryAssembly())); } public void SetDefaultAssembly(Assembly asm) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.cs new file mode 100644 index 0000000000..4c7a89cbda --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontFamilyTests.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; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Media; +using Avalonia.Media.Fonts; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class FontFamilyTests + { + [Fact] + public void Exception_Should_Be_Thrown_If_Name_Is_Null() + { + Assert.Throws(() => new FontFamily((string)null)); + } + + [Fact] + public void Exception_Should_Be_Thrown_If_Names_Is_Null() + { + Assert.Throws(() => new FontFamily((IEnumerable)null)); + } + + [Fact] + public void Should_Implicitly_Convert_String_To_FontFamily() + { + FontFamily fontFamily = "Arial"; + + Assert.Equal(new FontFamily("Arial"), fontFamily); + } + + [Fact] + public void Should_Be_Equal() + { + var fontFamily = new FontFamily("Arial"); + + Assert.Equal(new FontFamily("Arial"), fontFamily); + } + + [Fact] + public void Parse_Parses_FontFamily_With_Name() + { + var fontFamily = FontFamily.Parse("Courier New"); + + Assert.Equal("Courier New", fontFamily.Name); + } + + [Fact] + public void Parse_Parses_FontFamily_With_Names() + { + var fontFamily = FontFamily.Parse("Courier New, Times New Roman"); + + Assert.Equal("Courier New", fontFamily.Name); + + Assert.Equal(2, fontFamily.FamilyNames.Count()); + + Assert.Equal("Times New Roman", fontFamily.FamilyNames.Last()); + } + + [Fact] + public void Parse_Parses_FontFamily_With_Resource_Folder() + { + var source = new Uri("resm:Avalonia.Visuals.UnitTests#MyFont"); + + var key = new FontFamilyKey(source); + + var fontFamily = FontFamily.Parse(source.OriginalString); + + Assert.Equal("MyFont", fontFamily.Name); + + Assert.Equal(key, fontFamily.Key); + } + + [Fact] + public void Parse_Parses_FontFamily_With_Resource_Filename() + { + var source = new Uri("resm:Avalonia.Visuals.UnitTests.MyFont.ttf#MyFont"); + + var key = new FontFamilyKey(source); + + var fontFamily = FontFamily.Parse(source.OriginalString); + + Assert.Equal("MyFont", fontFamily.Name); + + Assert.Equal(key, fontFamily.Key); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FamilyNameCollectionTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FamilyNameCollectionTests.cs new file mode 100644 index 0000000000..14daf5bd8b --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FamilyNameCollectionTests.cs @@ -0,0 +1,33 @@ +// 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.Linq; +using Avalonia.Media.Fonts; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media.Fonts +{ + public class FamilyNameCollectionTests + { + [Fact] + public void Exception_Should_Be_Thrown_If_Names_Is_Null() + { + Assert.Throws(() => new FamilyNameCollection(null)); + } + + [Fact] + public void Exception_Should_Be_Thrown_If_Names_Is_Empty() + { + Assert.Throws(() => new FamilyNameCollection(Enumerable.Empty())); + } + + [Fact] + public void Should_Be_Equal() + { + var familyNames = new FamilyNameCollection(new[] { "Arial", "Times New Roman" }); + + Assert.Equal(new FamilyNameCollection(new[] { "Arial", "Times New Roman" }), familyNames); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs new file mode 100644 index 0000000000..96464b5784 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyKeyTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media.Fonts; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media.Fonts +{ + public class FontFamilyKeyTests + { + [Fact] + public void Exception_Should_Be_Thrown_If_Source_Is_Null() + { + Assert.Throws(() => new FontFamilyKey(null)); + } + + [Fact] + public void Should_Initialize_With_Location() + { + var source = new Uri("resm:Avalonia.Visuals.UnitTests#MyFont"); + + var fontFamilyKey = new FontFamilyKey(source); + + Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests"), fontFamilyKey.Location); + + Assert.Null(fontFamilyKey.FileName); + } + + [Fact] + public void Should_Initialize_With_Location_And_Filename() + { + var source = new Uri("resm:Avalonia.Visuals.UnitTests.MyFont.ttf#MyFont"); + + var fontFamilyKey = new FontFamilyKey(source); + + Assert.Equal(new Uri("resm:Avalonia.Visuals.UnitTests"), fontFamilyKey.Location); + + Assert.Equal("MyFont.ttf", fontFamilyKey.FileName); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs new file mode 100644 index 0000000000..19716431b9 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/Fonts/FontFamilyLoaderTests.cs @@ -0,0 +1,81 @@ +// 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.Linq; +using Avalonia.Media.Fonts; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media.Fonts +{ + using System.Diagnostics; + + public class FontFamilyLoaderTests + { + private const string FontName = "#MyFont"; + private const string Assembly = "?assembly=Avalonia.Visuals.UnitTests"; + private const string AssetLocation = "resm:Avalonia.Visuals.UnitTests.Assets"; + + private readonly IDisposable _testApplication; + + public FontFamilyLoaderTests() + { + const string AssetMyFontRegular = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName; + const string AssetMyFontBold = AssetLocation + ".MyFont-Bold.ttf" + Assembly + FontName; + const string AssetYourFont = AssetLocation + ".YourFont.ttf" + Assembly + FontName; + + var fontAssets = new[] + { + (AssetMyFontRegular, "AssetData"), + (AssetMyFontBold, "AssetData"), + (AssetYourFont, "AssetData") + }; + + _testApplication = StartWithResources(fontAssets); + } + + ~FontFamilyLoaderTests() + { + _testApplication.Dispose(); + } + + [Fact] + public void Should_Load_Single_FontAsset() + { + const string FontAsset = AssetLocation + ".MyFont-Regular.ttf" + Assembly + FontName; + + var source = new Uri(FontAsset, UriKind.RelativeOrAbsolute); + + var key = new FontFamilyKey(source); + + var fontAssets = FontFamilyLoader.LoadFontAssets(key); + + Assert.Single(fontAssets); + } + + [Fact] + public void Should_Load_Matching_Assets() + { + var source = new Uri(AssetLocation + ".MyFont-*.ttf" + Assembly + FontName, UriKind.RelativeOrAbsolute); + + var key = new FontFamilyKey(source); + + var fontAssets = FontFamilyLoader.LoadFontAssets(key).ToArray(); + + foreach (var fontAsset in fontAssets) + { + Debug.WriteLine(fontAsset); + } + + Assert.Equal(2, fontAssets.Length); + } + + private static IDisposable StartWithResources(params (string, string)[] assets) + { + var assetLoader = new MockAssetLoader(assets); + var services = new TestServices(assetLoader: assetLoader, platform: new AppBuilder().RuntimePlatform); + return UnitTestApplication.Start(services); + } + } +}