diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index f8767c7599..873048ef21 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 796bd8e596..08a9aa3ceb 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
index 3eb6d5b595..d50b051d9f 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
@@ -1,5 +1,7 @@
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.Media;
namespace ControlCatalog.Pages
{
@@ -14,7 +16,7 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
var fontComboBox = this.Find("fontComboBox");
- fontComboBox.Items = Avalonia.Media.FontFamily.SystemFontFamilies;
+ fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
fontComboBox.SelectedIndex = 0;
}
}
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 7f63e7725f..b17520a466 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -41,6 +41,9 @@
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml
new file mode 100644
index 0000000000..fb3e318a0e
--- /dev/null
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
new file mode 100644
index 0000000000..7f15845596
--- /dev/null
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -0,0 +1,80 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Threading;
+
+namespace RenderDemo.Pages
+{
+ public class GlyphRunPage : UserControl
+ {
+ private DrawingPresenter _drawingPresenter;
+ private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+ private readonly Random _rand = new Random();
+ private ushort[] _glyphIndices = new ushort[1];
+ private float _fontSize = 20;
+ private int _direction = 10;
+
+ public GlyphRunPage()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _drawingPresenter = this.FindControl("drawingPresenter");
+
+ DispatcherTimer.Run(() =>
+ {
+ UpdateGlyphRun();
+
+ return true;
+ }, TimeSpan.FromSeconds(1));
+ }
+
+ private void UpdateGlyphRun()
+ {
+ var c = (uint)_rand.Next(65, 90);
+
+ if (_fontSize + _direction > 200)
+ {
+ _direction = -10;
+ }
+
+ if (_fontSize + _direction < 20)
+ {
+ _direction = 10;
+ }
+
+ _fontSize += _direction;
+
+ _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+
+ var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
+
+ var drawingGroup = new DrawingGroup();
+
+ var glyphRunDrawing = new GlyphRunDrawing
+ {
+ Foreground = Brushes.Black,
+ GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
+ BaselineOrigin = new Point(0, -_glyphTypeface.Ascent * scale)
+ };
+
+ drawingGroup.Children.Add(glyphRunDrawing);
+
+ var geometryDrawing = new GeometryDrawing
+ {
+ Pen = new Pen(Brushes.Black),
+ Geometry = new RectangleGeometry { Rect = glyphRunDrawing.GlyphRun.Bounds }
+ };
+
+ drawingGroup.Children.Add(geometryDrawing);
+
+ _drawingPresenter.Drawing = drawingGroup;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
new file mode 100644
index 0000000000..a1246c57b5
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
@@ -0,0 +1,48 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Avalonia.Utilities
+{
+ public struct ImmutableReadOnlyListStructEnumerator : IEnumerator, IEnumerator
+ {
+ private readonly IReadOnlyList _readOnlyList;
+ private int _pos;
+
+ public ImmutableReadOnlyListStructEnumerator(IReadOnlyList readOnlyList)
+ {
+ _readOnlyList = readOnlyList;
+ _pos = -1;
+ Current = default;
+ }
+
+ public T Current
+ {
+ get;
+ private set;
+ }
+
+ object IEnumerator.Current => Current;
+
+ public void Dispose() { }
+
+ public bool MoveNext()
+ {
+ if (_pos >= _readOnlyList.Count - 1)
+ {
+ return false;
+ }
+
+ Current = _readOnlyList[++_pos];
+
+ return true;
+
+ }
+
+ public void Reset()
+ {
+ _pos = -1;
+
+ Current = default;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
index cd14211075..d27de7a80d 100644
--- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
+++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
@@ -188,8 +188,8 @@ namespace Avalonia.Controls.Presenters
break;
case NotifyCollectionChangedAction.Remove:
- if (e.OldStartingIndex >= FirstIndex &&
- e.OldStartingIndex < NextIndex)
+ if ((e.OldStartingIndex >= FirstIndex && e.OldStartingIndex < NextIndex) ||
+ panel.Children.Count > ItemCount)
{
RecycleContainersOnRemove();
}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index e0cc9aa128..9084012619 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -297,7 +297,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText
{
Text = "X",
- Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
+ Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
TextAlignment = TextAlignment,
Constraint = availableSize,
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index c7855ddfd1..8b8c7285be 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -352,7 +352,7 @@ namespace Avalonia.Controls
return new FormattedText
{
Constraint = constraint,
- Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
+ Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontWeight, FontStyle),
FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,
diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs
index 77902a7390..a9ce8ee494 100644
--- a/src/Avalonia.Input/FocusManager.cs
+++ b/src/Avalonia.Input/FocusManager.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@@ -17,8 +18,8 @@ namespace Avalonia.Input
///
/// The focus scopes in which the focus is currently defined.
///
- private readonly Dictionary _focusScopes =
- new Dictionary();
+ private readonly ConditionalWeakTable _focusScopes =
+ new ConditionalWeakTable();
///
/// Initializes a new instance of the class.
@@ -110,7 +111,18 @@ namespace Avalonia.Input
{
Contract.Requires(scope != null);
- _focusScopes[scope] = element;
+ if (_focusScopes.TryGetValue(scope, out IInputElement existingElement))
+ {
+ if (element != existingElement)
+ {
+ _focusScopes.Remove(scope);
+ _focusScopes.Add(scope, element);
+ }
+ }
+ else
+ {
+ _focusScopes.Add(scope, element);
+ }
if (Scope == scope)
{
diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs
index 92b7dae904..d05dbac574 100644
--- a/src/Avalonia.Visuals/Matrix.cs
+++ b/src/Avalonia.Visuals/Matrix.cs
@@ -306,7 +306,7 @@ namespace Avalonia
///
/// Parses a string.
///
- /// The string.
+ /// Six comma-delimited double values (m11, m12, m21, m22, offsetX, offsetY) that describe the new
/// The .
public static Matrix Parse(string s)
{
diff --git a/src/Avalonia.Visuals/Media/CharacterHit.cs b/src/Avalonia.Visuals/Media/CharacterHit.cs
new file mode 100644
index 0000000000..978a5b0c4c
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/CharacterHit.cs
@@ -0,0 +1,68 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Represents information about a character hit within a glyph run.
+ ///
+ ///
+ /// The CharacterHit structure provides information about the index of the first
+ /// character that got hit as well as information about leading or trailing edge.
+ ///
+ public readonly struct CharacterHit : IEquatable
+ {
+ ///
+ /// Initializes a new instance of the structure.
+ ///
+ /// Index of the first character that got hit.
+ /// In the case of a leading edge, this value is 0. In the case of a trailing edge,
+ /// this value is the number of code points until the next valid caret position.
+ public CharacterHit(int firstCharacterIndex, int trailingLength = 0)
+ {
+ FirstCharacterIndex = firstCharacterIndex;
+
+ TrailingLength = trailingLength;
+ }
+
+ ///
+ /// Gets the index of the first character that got hit.
+ ///
+ public int FirstCharacterIndex { get; }
+
+ ///
+ /// Gets the trailing length value for the character that got hit.
+ ///
+ public int TrailingLength { get; }
+
+ public bool Equals(CharacterHit other)
+ {
+ return FirstCharacterIndex == other.FirstCharacterIndex && TrailingLength == other.TrailingLength;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is CharacterHit other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return FirstCharacterIndex * 397 ^ TrailingLength;
+ }
+ }
+
+ public static bool operator ==(CharacterHit left, CharacterHit right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(CharacterHit left, CharacterHit right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index 8aa0bac41a..df69ab6fd5 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -187,6 +187,22 @@ namespace Avalonia.Media
}
}
+ ///
+ /// Draws a glyph run.
+ ///
+ /// The foreground brush.
+ /// The glyph run.
+ /// The baseline origin of the glyph run.
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ Contract.Requires(glyphRun != null);
+
+ if (foreground != null)
+ {
+ PlatformImpl.DrawGlyphRun(foreground, glyphRun, baselineOrigin);
+ }
+ }
+
///
/// Draws a filled rectangle.
///
diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs
index 771de524d9..a69a93e416 100644
--- a/src/Avalonia.Visuals/Media/FontFamily.cs
+++ b/src/Avalonia.Visuals/Media/FontFamily.cs
@@ -2,17 +2,17 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections.Generic;
-using System.Linq;
using Avalonia.Media.Fonts;
namespace Avalonia.Media
{
public sealed class FontFamily
{
+ public const string DefaultFontFamilyName = "$Default";
+
static FontFamily()
{
- Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
+ Default = new FontFamily(DefaultFontFamilyName);
}
///
@@ -57,15 +57,6 @@ namespace Avalonia.Media
///
public static FontFamily Default { get; }
- ///
- /// Represents all font families in the system. This can be an expensive call depending on platform implementation.
- ///
- ///
- /// Consider using the new instead.
- ///
- public static IEnumerable SystemFontFamilies =>
- FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
-
///
/// Gets the primary family name of the font family.
///
@@ -86,10 +77,16 @@ namespace Avalonia.Media
/// Gets the key for associated assets.
///
///
- /// The family familyNames.
+ /// The family key.
///
+ /// Key is only used for custom fonts.
public FontFamilyKey Key { get; }
+ ///
+ /// Returns True if this instance is the system's default.
+ ///
+ public bool IsDefault => Name.Equals(DefaultFontFamilyName);
+
///
/// Implicit conversion of string to FontFamily
///
@@ -188,6 +185,21 @@ namespace Avalonia.Media
}
}
+ public static bool operator !=(FontFamily a, FontFamily b)
+ {
+ return !(a == b);
+ }
+
+ public static bool operator ==(FontFamily a, FontFamily b)
+ {
+ if (ReferenceEquals(a, b))
+ {
+ return true;
+ }
+
+ return !(a is null) && a.Equals(b);
+ }
+
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs
index be1bd269ed..0c5e88b47a 100644
--- a/src/Avalonia.Visuals/Media/FontManager.cs
+++ b/src/Avalonia.Visuals/Media/FontManager.cs
@@ -1,8 +1,10 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
+using Avalonia.Media.Fonts;
using Avalonia.Platform;
namespace Avalonia.Media
@@ -11,9 +13,53 @@ namespace Avalonia.Media
/// The font manager is used to query the system's installed fonts and is responsible for caching loaded fonts.
/// It is also responsible for the font fallback.
///
- public abstract class FontManager
+ public sealed class FontManager
{
- public static readonly FontManager Default = CreateDefault();
+ private readonly ConcurrentDictionary _typefaceCache =
+ new ConcurrentDictionary();
+ private readonly FontFamily _defaultFontFamily;
+
+ private FontManager(IFontManagerImpl platformImpl)
+ {
+ PlatformImpl = platformImpl;
+
+ DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
+
+ _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
+ }
+
+ public static FontManager Current
+ {
+ get
+ {
+ var current = AvaloniaLocator.Current.GetService();
+
+ if (current != null)
+ {
+ return current;
+ }
+
+ var renderInterface = AvaloniaLocator.Current.GetService();
+
+ var fontManagerImpl = renderInterface?.CreateFontManager();
+
+ if (fontManagerImpl == null)
+ {
+ return null;
+ }
+
+ current = new FontManager(fontManagerImpl);
+
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(current);
+
+ return current;
+ }
+ }
+
+ ///
+ ///
+ ///
+ public IFontManagerImpl PlatformImpl { get; }
///
/// Gets the system's default font family's name.
@@ -21,25 +67,55 @@ namespace Avalonia.Media
public string DefaultFontFamilyName
{
get;
- protected set;
}
///
- /// Get all installed fonts in the system.
- /// If true the font collection is updated.
+ /// Get all installed font family names.
///
- public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false);
+ /// If true the font collection is updated.
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
+ PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
///
- /// Get a cached typeface from specified parameters.
+ /// Returns a new typeface, or an existing one if a matching typeface exists.
///
/// The font family.
/// The font weight.
/// The font style.
///
- /// The cached typeface.
+ /// The typeface.
///
- public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
+ public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal)
+ {
+ while (true)
+ {
+ if (fontFamily.IsDefault)
+ {
+ fontFamily = _defaultFontFamily;
+ }
+
+ var key = new FontKey(fontFamily, fontWeight, fontStyle);
+
+ if (_typefaceCache.TryGetValue(key, out var typeface))
+ {
+ return typeface;
+ }
+
+ typeface = new Typeface(fontFamily, fontWeight, fontStyle);
+
+ if (_typefaceCache.TryAdd(key, typeface))
+ {
+ return typeface;
+ }
+
+ if (fontFamily == _defaultFontFamily)
+ {
+ return null;
+ }
+
+ fontFamily = _defaultFontFamily;
+ }
+ }
///
/// Tries to match a specified character to a typeface that supports specified font properties.
@@ -53,60 +129,13 @@ namespace Avalonia.Media
///
/// The matched typeface.
///
- public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
- FontStyle fontStyle = default,
- FontFamily fontFamily = null, CultureInfo culture = null);
-
- public static FontManager CreateDefault()
- {
- var platformImpl = AvaloniaLocator.Current.GetService();
-
- if (platformImpl != null)
- {
- return new PlatformFontManager(platformImpl);
- }
-
- return new EmptyFontManager();
- }
-
- private class PlatformFontManager : FontManager
+ public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal,
+ FontStyle fontStyle = FontStyle.Normal,
+ FontFamily fontFamily = null, CultureInfo culture = null)
{
- private readonly IFontManagerImpl _platformImpl;
-
- public PlatformFontManager(IFontManagerImpl platformImpl)
- {
- _platformImpl = platformImpl;
-
- DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
- }
-
- public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
- _platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
-
- public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) =>
- _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle);
-
- public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
- FontStyle fontStyle = default,
- FontFamily fontFamily = null, CultureInfo culture = null) =>
- _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture);
- }
-
- private class EmptyFontManager : FontManager
- {
- public EmptyFontManager()
- {
- DefaultFontFamilyName = "Empty";
- }
-
- public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
- new[] { DefaultFontFamilyName };
-
- public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) => new Typeface(fontFamily, fontWeight, fontStyle);
-
- public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
- FontStyle fontStyle = default,
- FontFamily fontFamily = null, CultureInfo culture = null) => null;
+ return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
+ _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
+ null;
}
}
}
diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
index eb0faf4187..cd08bba7b2 100644
--- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
@@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
+using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
@@ -54,25 +55,19 @@ namespace Avalonia.Media.Fonts
///
internal IReadOnlyList Names { get; }
- ///
///
- /// Returns an enumerator that iterates through the collection.
+ /// Returns an enumerator for the name collection.
///
- ///
- /// An enumerator that can be used to iterate through the collection.
- ///
- public IEnumerator GetEnumerator()
+ public ImmutableReadOnlyListStructEnumerator GetEnumerator()
{
- return Names.GetEnumerator();
+ return new ImmutableReadOnlyListStructEnumerator(this);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
}
- ///
- ///
- /// Returns an enumerator that iterates through a collection.
- ///
- ///
- /// An object that can be used to iterate through the collection.
- ///
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
@@ -131,6 +126,21 @@ namespace Avalonia.Media.Fonts
}
}
+ public static bool operator !=(FamilyNameCollection a, FamilyNameCollection b)
+ {
+ return !(a == b);
+ }
+
+ public static bool operator ==(FamilyNameCollection a, FamilyNameCollection b)
+ {
+ if (ReferenceEquals(a, b))
+ {
+ return true;
+ }
+
+ return !(a is null) && a.Equals(b);
+ }
+
///
/// Determines whether the specified , is equal to this instance.
///
diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
index 7733dd7d2a..887862face 100644
--- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs
@@ -58,6 +58,21 @@ namespace Avalonia.Media.Fonts
}
}
+ public static bool operator !=(FontFamilyKey a, FontFamilyKey b)
+ {
+ return !(a == b);
+ }
+
+ public static bool operator ==(FontFamilyKey a, FontFamilyKey b)
+ {
+ if (ReferenceEquals(a, b))
+ {
+ return true;
+ }
+
+ return !(a is null) && a.Equals(b);
+ }
+
///
/// Determines whether the specified , is equal to this instance.
///
diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
index 063fe8f20d..bed1fc6b83 100644
--- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyLoader.cs
@@ -10,13 +10,6 @@ namespace Avalonia.Media.Fonts
{
public static class FontFamilyLoader
{
- private static readonly IAssetLoader s_assetLoader;
-
- static FontFamilyLoader()
- {
- s_assetLoader = AvaloniaLocator.Current.GetService();
- }
-
///
/// Loads all font assets that belong to the specified
///
@@ -42,7 +35,9 @@ namespace Avalonia.Media.Fonts
///
private static IEnumerable GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
{
- var availableAssets = s_assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
+ var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
var matchingAssets =
availableAssets.Where(x => x.AbsolutePath.EndsWith(".ttf") || x.AbsolutePath.EndsWith(".otf"));
@@ -58,9 +53,11 @@ namespace Avalonia.Media.Fonts
///
private static IEnumerable GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
{
+ var assetLoader = AvaloniaLocator.Current.GetService();
+
var fileName = GetFileName(fontFamilyKey, out var fileExtension, out var location);
- var availableResources = s_assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
+ var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
string compareTo;
diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs
similarity index 65%
rename from src/Skia/Avalonia.Skia/FontKey.cs
rename to src/Avalonia.Visuals/Media/Fonts/FontKey.cs
index bb3fe230c1..0ead585612 100644
--- a/src/Skia/Avalonia.Skia/FontKey.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs
@@ -2,24 +2,26 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using Avalonia.Media;
-namespace Avalonia.Skia
+namespace Avalonia.Media.Fonts
{
- internal readonly struct FontKey : IEquatable
+ public readonly struct FontKey : IEquatable
{
+ public readonly FontFamily FontFamily;
public readonly FontStyle Style;
public readonly FontWeight Weight;
- public FontKey(FontWeight weight, FontStyle style)
+ public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
{
+ FontFamily = fontFamily;
Style = style;
Weight = weight;
}
public override int GetHashCode()
{
- var hash = 17;
+ var hash = FontFamily.GetHashCode();
+
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
@@ -33,7 +35,8 @@ namespace Avalonia.Skia
public bool Equals(FontKey other)
{
- return Style == other.Style &&
+ return FontFamily == other.FontFamily &&
+ Style == other.Style &&
Weight == other.Weight;
}
}
diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs
new file mode 100644
index 0000000000..43151deece
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphRun.cs
@@ -0,0 +1,457 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using Avalonia.Platform;
+using Avalonia.Utility;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Represents a sequence of glyphs from a single face of a single font at a single size, and with a single rendering style.
+ ///
+ public sealed class GlyphRun : IDisposable
+ {
+ private static readonly IPlatformRenderInterface s_platformRenderInterface =
+ AvaloniaLocator.Current.GetService();
+
+ private IGlyphRunImpl _glyphRunImpl;
+ private GlyphTypeface _glyphTypeface;
+ private double _fontRenderingEmSize;
+ private Rect? _bounds;
+
+ private ReadOnlySlice _glyphIndices;
+ private ReadOnlySlice _glyphAdvances;
+ private ReadOnlySlice _glyphOffsets;
+ private ReadOnlySlice _glyphClusters;
+ private ReadOnlySlice _characters;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GlyphRun()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the class by specifying properties of the class.
+ ///
+ /// The glyph typeface.
+ /// The rendering em size.
+ /// The glyph indices.
+ /// The glyph advances.
+ /// The glyph offsets.
+ /// The characters.
+ /// The glyph clusters.
+ /// The bidi level.
+ /// The bound.
+ public GlyphRun(
+ GlyphTypeface glyphTypeface,
+ double fontRenderingEmSize,
+ ReadOnlySlice glyphIndices,
+ ReadOnlySlice glyphAdvances = default,
+ ReadOnlySlice glyphOffsets = default,
+ ReadOnlySlice characters = default,
+ ReadOnlySlice glyphClusters = default,
+ int bidiLevel = 0,
+ Rect? bounds = null)
+ {
+ GlyphTypeface = glyphTypeface;
+
+ FontRenderingEmSize = fontRenderingEmSize;
+
+ GlyphIndices = glyphIndices;
+
+ GlyphAdvances = glyphAdvances;
+
+ GlyphOffsets = glyphOffsets;
+
+ Characters = characters;
+
+ GlyphClusters = glyphClusters;
+
+ BidiLevel = bidiLevel;
+
+ Initialize(bounds);
+ }
+
+ ///
+ /// Gets or sets the for the .
+ ///
+ public GlyphTypeface GlyphTypeface
+ {
+ get => _glyphTypeface;
+ set => Set(ref _glyphTypeface, value);
+ }
+
+ ///
+ /// Gets or sets the em size used for rendering the .
+ ///
+ public double FontRenderingEmSize
+ {
+ get => _fontRenderingEmSize;
+ set => Set(ref _fontRenderingEmSize, value);
+ }
+
+ ///
+ /// Gets or sets an array of values that represent the glyph indices in the rendering physical font.
+ ///
+ public ReadOnlySlice GlyphIndices
+ {
+ get => _glyphIndices;
+ set => Set(ref _glyphIndices, value);
+ }
+
+ ///
+ /// Gets or sets an array of values that represent the advances corresponding to the glyph indices.
+ ///
+ public ReadOnlySlice GlyphAdvances
+ {
+ get => _glyphAdvances;
+ set => Set(ref _glyphAdvances, value);
+ }
+
+ ///
+ /// Gets or sets an array of values representing the offsets of the glyphs in the .
+ ///
+ public ReadOnlySlice GlyphOffsets
+ {
+ get => _glyphOffsets;
+ set => Set(ref _glyphOffsets, value);
+ }
+
+ ///
+ /// Gets or sets the list of UTF16 code points that represent the Unicode content of the .
+ ///
+ public ReadOnlySlice Characters
+ {
+ get => _characters;
+ set => Set(ref _characters, value);
+ }
+
+ ///
+ /// Gets or sets a list of values representing a mapping from character index to glyph index.
+ ///
+ public ReadOnlySlice GlyphClusters
+ {
+ get => _glyphClusters;
+ set => Set(ref _glyphClusters, value);
+ }
+
+ ///
+ /// Gets or sets the bidirectional nesting level of the .
+ ///
+ public int BidiLevel
+ {
+ get;
+ set;
+ }
+
+ ///
+ ///
+ ///
+ internal double Scale => FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
+
+ ///
+ ///
+ ///
+ internal bool IsLeftToRight => ((BidiLevel & 1) == 0);
+
+ ///
+ /// Gets or sets the conservative bounding box of the .
+ ///
+ public Rect Bounds
+ {
+ get
+ {
+ if (_bounds == null)
+ {
+ _bounds = CalculateBounds();
+ }
+
+ return _bounds.Value;
+ }
+ set => _bounds = value;
+ }
+
+ public IGlyphRunImpl GlyphRunImpl
+ {
+ get
+ {
+ if (_glyphRunImpl == null)
+ {
+ Initialize(null);
+ }
+
+ return _glyphRunImpl;
+ }
+ }
+
+ public double GetDistanceFromCharacterHit(CharacterHit characterHit)
+ {
+ var distance = 0.0;
+
+ var end = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ for (var i = 0; i < _glyphClusters.Length; i++)
+ {
+ if (_glyphClusters[i] >= end)
+ {
+ break;
+ }
+
+ if (GlyphAdvances.IsEmpty)
+ {
+ var glyph = GlyphIndices[i];
+
+ distance += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ else
+ {
+ distance += GlyphAdvances[i];
+ }
+ }
+
+ return distance;
+ }
+
+ public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
+ {
+ // Before
+ if (distance < 0)
+ {
+ isInside = false;
+
+ var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _);
+
+ return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
+ }
+
+ //After
+ if (distance > Bounds.Size.Width)
+ {
+ isInside = false;
+
+ var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _);
+
+ return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
+ }
+
+ //Within
+ var currentX = 0.0;
+ var index = 0;
+
+ for (; index < GlyphIndices.Length; index++)
+ {
+ if (GlyphAdvances.IsEmpty)
+ {
+ var glyph = GlyphIndices[index];
+
+ currentX += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ else
+ {
+ currentX += GlyphAdvances[index];
+ }
+
+ if (currentX > distance)
+ {
+ break;
+ }
+ }
+
+ if (index == GlyphIndices.Length)
+ {
+ index--;
+ }
+
+ var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width);
+
+ isInside = distance < currentX && width > 0;
+
+ var isTrailing = distance > currentX - width / 2;
+
+ return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
+ }
+
+ public CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
+ {
+ if (characterHit.TrailingLength == 0)
+ {
+ return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
+ }
+
+ var nextCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
+
+ return new CharacterHit(nextCharacterHit.FirstCharacterIndex);
+ }
+
+ public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
+ {
+ return characterHit.TrailingLength == 0 ?
+ FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _) :
+ new CharacterHit(characterHit.FirstCharacterIndex);
+ }
+
+ private class ReverseComparer : IComparer
+ {
+ public int Compare(T x, T y)
+ {
+ return Comparer.Default.Compare(y, x);
+ }
+ }
+
+ private static readonly IComparer s_ascendingComparer = Comparer.Default;
+ private static readonly IComparer s_descendingComparer = new ReverseComparer();
+
+ internal CharacterHit FindNearestCharacterHit(int index, out double width)
+ {
+ width = 0.0;
+
+ if (index < 0)
+ {
+ return default;
+ }
+
+ var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
+
+ var clusters = _glyphClusters.AsSpan();
+
+ int start;
+
+ if (index == 0 && clusters[0] == 0)
+ {
+ start = 0;
+ }
+ else
+ {
+ // Find the start of the cluster at the character index.
+ start = clusters.BinarySearch((ushort)index, comparer);
+ }
+
+ // No cluster found.
+ if (start < 0)
+ {
+ while (index > 0 && start < 0)
+ {
+ index--;
+
+ start = clusters.BinarySearch((ushort)index, comparer);
+ }
+
+ if (start < 0)
+ {
+ return default;
+ }
+ }
+
+ var trailingLength = 0;
+
+ var currentCluster = clusters[start];
+
+ while (start > 0 && clusters[start - 1] == currentCluster)
+ {
+ start--;
+ }
+
+ for (var lastIndex = start; lastIndex < _glyphClusters.Length; ++lastIndex)
+ {
+ if (_glyphClusters[lastIndex] != currentCluster)
+ {
+ break;
+ }
+
+ if (GlyphAdvances.IsEmpty)
+ {
+ var glyph = GlyphIndices[lastIndex];
+
+ width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ else
+ {
+ width += GlyphAdvances[lastIndex];
+ }
+
+ trailingLength++;
+ }
+
+ return new CharacterHit(currentCluster, trailingLength);
+ }
+
+ private Rect CalculateBounds()
+ {
+ var scale = FontRenderingEmSize / GlyphTypeface.DesignEmHeight;
+
+ var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * scale;
+
+ var width = 0.0;
+
+ if (GlyphAdvances.IsEmpty)
+ {
+ foreach (var glyph in GlyphIndices)
+ {
+ width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
+ }
+ }
+ else
+ {
+ foreach (var advance in GlyphAdvances)
+ {
+ width += advance;
+ }
+ }
+
+ return new Rect(0, 0, width, height);
+ }
+
+ private void Set(ref T field, T value)
+ {
+ if (_glyphRunImpl != null)
+ {
+ throw new InvalidOperationException("GlyphRun can't be changed after it has been initialized.'");
+ }
+
+ field = value;
+ }
+
+ private void Initialize(Rect? bounds)
+ {
+ if (GlyphIndices.Length == 0)
+ {
+ throw new InvalidOperationException();
+ }
+
+ var glyphCount = GlyphIndices.Length;
+
+ if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount)
+ {
+ throw new InvalidOperationException();
+ }
+
+ if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount)
+ {
+ throw new InvalidOperationException();
+ }
+
+ _glyphRunImpl = s_platformRenderInterface.CreateGlyphRun(this, out var width);
+
+ if (bounds.HasValue)
+ {
+ _bounds = bounds;
+ }
+ else
+ {
+ var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
+
+ _bounds = new Rect(0, 0, width, height);
+ }
+ }
+
+ void IDisposable.Dispose()
+ {
+ _glyphRunImpl?.Dispose();
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
new file mode 100644
index 0000000000..22d6e20b34
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphRunDrawing.cs
@@ -0,0 +1,50 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Media
+{
+ public class GlyphRunDrawing : Drawing
+ {
+ public static readonly StyledProperty ForegroundProperty =
+ AvaloniaProperty.Register(nameof(Foreground));
+
+ public static readonly StyledProperty GlyphRunProperty =
+ AvaloniaProperty.Register(nameof(GlyphRun));
+
+ public static readonly StyledProperty BaselineOriginProperty =
+ AvaloniaProperty.Register(nameof(BaselineOrigin));
+
+ public IBrush Foreground
+ {
+ get => GetValue(ForegroundProperty);
+ set => SetValue(ForegroundProperty, value);
+ }
+
+ public GlyphRun GlyphRun
+ {
+ get => GetValue(GlyphRunProperty);
+ set => SetValue(GlyphRunProperty, value);
+ }
+
+ public Point BaselineOrigin
+ {
+ get => GetValue(BaselineOriginProperty);
+ set => SetValue(BaselineOriginProperty, value);
+ }
+
+ public override void Draw(DrawingContext context)
+ {
+ if (GlyphRun == null)
+ {
+ return;
+ }
+
+ context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
+ }
+
+ public override Rect GetBounds()
+ {
+ return GlyphRun?.Bounds ?? default;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
index 3ba31f7e84..6468f701d6 100644
--- a/src/Avalonia.Visuals/Media/GlyphTypeface.cs
+++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
@@ -9,11 +9,9 @@ namespace Avalonia.Media
{
public sealed class GlyphTypeface : IDisposable
{
- private static readonly IPlatformRenderInterface s_platformRenderInterface =
- AvaloniaLocator.Current.GetService();
-
- public GlyphTypeface(Typeface typeface) : this(s_platformRenderInterface.CreateGlyphTypeface(typeface))
+ public GlyphTypeface(Typeface typeface)
{
+ PlatformImpl = FontManager.Current?.PlatformImpl.CreateGlyphTypeface(typeface);
}
public GlyphTypeface(IGlyphTypefaceImpl platformImpl)
@@ -68,6 +66,11 @@ namespace Avalonia.Media
///
public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
+ ///
+ /// A value indicating whether all glyphs in the font have the same advancement.
+ ///
+ public bool IsFixedPitch => PlatformImpl.IsFixedPitch;
+
///
/// Returns an glyph index for the specified codepoint.
///
diff --git a/src/Avalonia.Visuals/Media/Transform.cs b/src/Avalonia.Visuals/Media/Transform.cs
index 7a70657ce0..8253d11ff1 100644
--- a/src/Avalonia.Visuals/Media/Transform.cs
+++ b/src/Avalonia.Visuals/Media/Transform.cs
@@ -28,6 +28,16 @@ namespace Avalonia.Media
///
public abstract Matrix Value { get; }
+ ///
+ /// Parses a string.
+ ///
+ /// Six comma-delimited double values that describe the new . For details check
+ /// The .
+ public static Transform Parse(string s)
+ {
+ return new MatrixTransform(Matrix.Parse(s));
+ }
+
///
/// Raises the event.
///
@@ -35,5 +45,14 @@ namespace Avalonia.Media
{
Changed?.Invoke(this, EventArgs.Empty);
}
+
+ ///
+ /// Returns a String representing this transform matrix instance.
+ ///
+ /// The string representation.
+ public override string ToString()
+ {
+ return Value.ToString();
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs
index a6d5c8a43c..9a17bad7d2 100644
--- a/src/Avalonia.Visuals/Media/Typeface.cs
+++ b/src/Avalonia.Visuals/Media/Typeface.cs
@@ -13,8 +13,6 @@ namespace Avalonia.Media
[DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
public class Typeface : IEquatable
{
- public static readonly Typeface Default = new Typeface(FontFamily.Default);
-
private GlyphTypeface _glyphTypeface;
///
@@ -50,6 +48,8 @@ namespace Avalonia.Media
{
}
+ public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default);
+
///
/// Gets the font family.
///
diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
index 5edb1c9760..f2309c271d 100644
--- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
@@ -86,6 +86,14 @@ namespace Avalonia.Platform
/// The text.
void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
+ ///
+ /// Draws a glyph run.
+ ///
+ /// The foreground.
+ /// The glyph run.
+ /// The baseline origin of the glyph run.
+ void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin);
+
///
/// Creates a new that can be used as a render layer
/// for the current render target.
diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
index 254b5d07d1..a8e6dcb29b 100644
--- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
+using Avalonia.Media.Fonts;
namespace Avalonia.Platform
{
@@ -12,7 +13,7 @@ namespace Avalonia.Platform
///
/// Gets the system's default font family's name.
///
- string DefaultFontFamilyName { get; }
+ string GetDefaultFontFamilyName();
///
/// Get all installed fonts in the system.
@@ -20,17 +21,6 @@ namespace Avalonia.Platform
///
IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false);
- ///
- /// Get a typeface from specified parameters.
- ///
- /// The font family.
- /// The font weight.
- /// The font style.
- ///
- /// The typeface.
- ///
- Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
-
///
/// Tries to match a specified character to a typeface that supports specified font properties.
///
@@ -39,10 +29,20 @@ namespace Avalonia.Platform
/// The font style.
/// The font family. This is optional and used for fallback lookup.
/// The culture.
+ /// The matching font key.
///
- /// The typeface.
+ /// True, if the could match the character to specified parameters, False otherwise.
+ ///
+ bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
+ FontFamily fontFamily, CultureInfo culture, out FontKey fontKey);
+
+ ///
+ /// Creates a glyph typeface.
+ ///
+ /// The typeface.
+ /// 0
+ /// The created glyph typeface. Can be Null if it was not possible to create a glyph typeface.
///
- Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
- FontFamily fontFamily = null, CultureInfo culture = null);
+ IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
}
}
diff --git a/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
new file mode 100644
index 0000000000..0f1359794a
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/IGlyphRunImpl.cs
@@ -0,0 +1,12 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Platform
+{
+ ///
+ /// Actual implementation of a glyph run that stores platform dependent resources.
+ ///
+ public interface IGlyphRunImpl : IDisposable { }
+}
diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
index 8c043a5129..5d6ff23c0a 100644
--- a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
+++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
@@ -47,6 +47,11 @@ namespace Avalonia.Platform
///
int StrikethroughThickness { get; }
+ ///
+ /// A value indicating whether all glyphs in the font have the same advancement.
+ ///
+ bool IsFixedPitch { get; }
+
///
/// Returns an glyph index for the specified codepoint.
///
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index 5a0a7b2f19..7ae0eaf8f2 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -113,12 +113,17 @@ namespace Avalonia.Platform
IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
///
- /// Creates a glyph typeface for specified typeface.
+ /// Creates a font manager implementation.
///
- /// The typeface.
- ///
- /// The glyph typeface implementation.
- ///
- IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
+ /// The font manager.
+ IFontManagerImpl CreateFontManager();
+
+ ///
+ /// Creates a platform implementation of a glyph run.
+ ///
+ /// The glyph run.
+ /// The glyph run's width.
+ ///
+ IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
}
}
diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs
index d92f8b0fc4..27ac7a3026 100644
--- a/src/Avalonia.Visuals/Point.cs
+++ b/src/Avalonia.Visuals/Point.cs
@@ -175,7 +175,7 @@ namespace Avalonia
/// Parses a string.
///
/// The string.
- /// The .
+ /// The .
public static Point Parse(string s)
{
using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Point."))
diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs
index 2e8fb16bc1..ebd0ba9351 100644
--- a/src/Avalonia.Visuals/RelativePoint.cs
+++ b/src/Avalonia.Visuals/RelativePoint.cs
@@ -177,5 +177,16 @@ namespace Avalonia
unit);
}
}
+
+ ///
+ /// Returns a String representing this RelativePoint instance.
+ ///
+ /// The string representation.
+ public override string ToString()
+ {
+ return _unit == RelativeUnit.Absolute ?
+ _point.ToString() :
+ string.Format(CultureInfo.InvariantCulture, "{0}%, {1}%", _point.X * 100, _point.Y * 100);
+ }
}
}
diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs
index e39581fc57..1e7b5c2923 100644
--- a/src/Avalonia.Visuals/Rendering/RendererBase.cs
+++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs
@@ -7,7 +7,6 @@ namespace Avalonia.Rendering
{
public class RendererBase
{
- private static readonly Typeface s_fpsTypeface = new Typeface("Arial");
private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
@@ -19,7 +18,7 @@ namespace Avalonia.Rendering
{
_fpsText = new FormattedText
{
- Typeface = s_fpsTypeface,
+ Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default),
FontSize = s_fontSize
};
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
index 4fbfb02660..a169a629be 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
@@ -190,6 +190,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
+ ///
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ var next = NextDrawAs();
+
+ if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
+ {
+ Add(new GlyphRunNode(Transform, foreground, glyphRun, baselineOrigin, CreateChildScene(foreground)));
+ }
+
+ else
+ {
+ ++_drawOperationindex;
+ }
+ }
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
new file mode 100644
index 0000000000..b3c4fdbac0
--- /dev/null
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
@@ -0,0 +1,91 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Collections.Generic;
+
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+ ///
+ /// A node in the scene graph which represents a glyph run draw.
+ ///
+ internal class GlyphRunNode : BrushDrawOperation
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The transform.
+ /// The foreground brush.
+ /// The glyph run to draw.
+ /// The baseline origin of the glyph run.
+ /// Child scenes for drawing visual brushes.
+ public GlyphRunNode(
+ Matrix transform,
+ IBrush foreground,
+ GlyphRun glyphRun,
+ Point baselineOrigin,
+ IDictionary childScenes = null)
+ : base(glyphRun.Bounds, transform, null)
+ {
+ Transform = transform;
+ Foreground = foreground?.ToImmutable();
+ GlyphRun = glyphRun;
+ BaselineOrigin = baselineOrigin;
+ ChildScenes = childScenes;
+ }
+
+ ///
+ /// Gets the transform with which the node will be drawn.
+ ///
+ public Matrix Transform { get; }
+
+ ///
+ /// Gets the foreground brush.
+ ///
+ public IBrush Foreground { get; }
+
+ ///
+ /// Gets the glyph run to draw.
+ ///
+ public GlyphRun GlyphRun { get; }
+
+ ///
+ /// Gets the baseline origin.
+ ///
+ public Point BaselineOrigin { get; set; }
+
+ ///
+ public override IDictionary ChildScenes { get; }
+
+ ///
+ public override void Render(IDrawingContextImpl context)
+ {
+ context.Transform = Transform;
+ context.DrawGlyphRun(Foreground, GlyphRun, BaselineOrigin);
+ }
+
+ ///
+ /// Determines if this draw operation equals another.
+ ///
+ /// The transform of the other draw operation.
+ /// The foreground of the other draw operation.
+ /// The glyph run of the other draw operation.
+ /// True if the draw operations are the same, otherwise false.
+ ///
+ /// The properties of the other draw operation are passed in as arguments to prevent
+ /// allocation of a not-yet-constructed draw operation object.
+ ///
+ internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun)
+ {
+ return transform == Transform &&
+ Equals(foreground, Foreground) &&
+ Equals(glyphRun, GlyphRun);
+ }
+
+ ///
+ public override bool HitTest(Point p) => Bounds.Contains(p);
+ }
+}
diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
new file mode 100644
index 0000000000..c54ccc8ef1
--- /dev/null
+++ b/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs
@@ -0,0 +1,154 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Utilities;
+
+namespace Avalonia.Utility
+{
+ ///
+ /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
+ ///
+ /// The type of elements in the slice.
+ public readonly struct ReadOnlySlice : IReadOnlyList
+ {
+ public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { }
+
+ public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length)
+ {
+ Buffer = buffer;
+ Start = start;
+ Length = length;
+ }
+
+ ///
+ /// Gets the start.
+ ///
+ ///
+ /// The start.
+ ///
+ public int Start { get; }
+
+ ///
+ /// Gets the end.
+ ///
+ ///
+ /// The end.
+ ///
+ public int End => Start + Length - 1;
+
+ ///
+ /// Gets the length.
+ ///
+ ///
+ /// The length.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets a value that indicates whether this instance of is Empty.
+ ///
+ public bool IsEmpty => Length == 0;
+
+ ///
+ /// The buffer.
+ ///
+ public ReadOnlyMemory Buffer { get; }
+
+ public T this[int index] => Buffer.Span[Start + index];
+
+ ///
+ /// Returns a span of the underlying buffer.
+ ///
+ /// The of the underlying buffer.
+ public ReadOnlySpan AsSpan()
+ {
+ return Buffer.Span.Slice(Start, Length);
+ }
+
+ ///
+ /// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
+ ///
+ /// The start of the sub slice.
+ /// The length of the sub slice.
+ /// A that contains the specified number of elements from the specified start.
+ public ReadOnlySlice AsSlice(int start, int length)
+ {
+ if (start < 0 || start >= Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start));
+ }
+
+ if (Start + start > End)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ReadOnlySlice(Buffer, Start + start, length);
+ }
+
+ ///
+ /// Returns a specified number of contiguous elements from the start of the slice.
+ ///
+ /// The number of elements to return.
+ /// A that contains the specified number of elements from the start of this slice.
+ public ReadOnlySlice Take(int length)
+ {
+ if (length > Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ReadOnlySlice(Buffer, Start, length);
+ }
+
+ ///
+ /// Bypasses a specified number of elements in the slice and then returns the remaining elements.
+ ///
+ /// The number of elements to skip before returning the remaining elements.
+ /// A that contains the elements that occur after the specified index in this slice.
+ public ReadOnlySlice Skip(int length)
+ {
+ if (length > Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return new ReadOnlySlice(Buffer, Start + length, Length - length);
+ }
+
+ ///
+ /// Returns an enumerator for the slice.
+ ///
+ public ImmutableReadOnlyListStructEnumerator GetEnumerator()
+ {
+ return new ImmutableReadOnlyListStructEnumerator(this);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ int IReadOnlyCollection.Count => Length;
+
+ T IReadOnlyList.this[int index] => this[index];
+
+ public static implicit operator ReadOnlySlice(T[] array)
+ {
+ return new ReadOnlySlice(array);
+ }
+
+ public static implicit operator ReadOnlySlice(ReadOnlyMemory memory)
+ {
+ return new ReadOnlySlice(memory);
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs
index 576d2daaaa..d99fbe8e65 100644
--- a/src/Avalonia.Visuals/Vector.cs
+++ b/src/Avalonia.Visuals/Vector.cs
@@ -4,6 +4,7 @@
using System;
using System.Globalization;
using Avalonia.Animation.Animators;
+using Avalonia.Utilities;
using JetBrains.Annotations;
namespace Avalonia
@@ -85,6 +86,22 @@ namespace Avalonia
public static Vector operator /(Vector vector, double scale)
=> Divide(vector, scale);
+ ///
+ /// Parses a string.
+ ///
+ /// The string.
+ /// The .
+ public static Vector Parse(string s)
+ {
+ using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Vector."))
+ {
+ return new Vector(
+ tokenizer.ReadDouble(),
+ tokenizer.ReadDouble()
+ );
+ }
+ }
+
///
/// Length of the vector
///
@@ -166,9 +183,9 @@ namespace Avalonia
}
///
- /// Returns the string representation of the point.
+ /// Returns the string representation of the vector.
///
- /// The string representation of the point.
+ /// The string representation of the vector.
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
index aab43bbd6f..40386924c3 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs
@@ -25,7 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if ((tt?.Values.FirstOrDefault() is XamlIlTypeExtensionNode tn))
{
- targetType = tn.Type;
+ targetType = tn.Value;
}
else
{
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
index ad9915e193..4c4b6cf8ff 160000
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github
@@ -1 +1 @@
-Subproject commit ad9915e19398a49c5a11b66000c361659ca692b3
+Subproject commit 4c4b6cf8ff0894c925d87b27d4fc7a064440c218
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index 835c377791..d06cfa69a7 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -232,6 +232,20 @@ namespace Avalonia.Skia
}
}
+ ///
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
+ {
+ var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
+
+ paint.ApplyTo(glyphRunImpl.Paint);
+
+ Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
+ (float)baselineOrigin.Y, glyphRunImpl.Paint);
+ }
+ }
+
///
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
index 03de82178a..727947e59d 100644
--- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -1,9 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
+using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SkiaSharp;
@@ -13,13 +15,11 @@ namespace Avalonia.Skia
{
private SKFontManager _skFontManager = SKFontManager.Default;
- public FontManagerImpl()
+ public string GetDefaultFontFamilyName()
{
- DefaultFontFamilyName = SKTypeface.Default.FamilyName;
+ return SKTypeface.Default.FamilyName;
}
- public string DefaultFontFamilyName { get; }
-
public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
if (checkForUpdates)
@@ -30,53 +30,86 @@ namespace Avalonia.Skia
return _skFontManager.FontFamilies;
}
- public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
- {
- return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
- }
+ [ThreadStatic] private static string[] t_languageTagBuffer;
- public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
- FontFamily fontFamily = null, CultureInfo culture = null)
+ public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
+ FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
- var fontFamilyName = FontFamily.Default.Name;
-
if (culture == null)
{
culture = CultureInfo.CurrentUICulture;
}
+ if (t_languageTagBuffer == null)
+ {
+ t_languageTagBuffer = new string[2];
+ }
+
+ t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
+ t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
+
if (fontFamily != null)
{
foreach (var familyName in fontFamily.FamilyNames)
{
var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
- SKFontStyleWidth.Normal,
- (SKFontStyleSlant)fontStyle,
- new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
+ SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null)
{
continue;
}
- fontFamilyName = familyName;
+ fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
- break;
+ return true;
}
}
else
{
- var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
- (SKFontStyleSlant)fontStyle,
- new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
+ var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
+ SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null)
{
- fontFamilyName = skTypeface.FamilyName;
+ fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
+
+ return true;
+ }
+ }
+
+ fontKey = default;
+
+ return false;
+ }
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ var skTypeface = SKTypeface.Default;
+
+ if (typeface.FontFamily.Key == null)
+ {
+ foreach (var familyName in typeface.FontFamily.FamilyNames)
+ {
+ skTypeface = SKTypeface.FromFamilyName(familyName, (SKFontStyleWeight)typeface.Weight,
+ SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
+
+ if (skTypeface == SKTypeface.Default)
+ {
+ continue;
+ }
+
+ break;
}
}
+ else
+ {
+ var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
+
+ skTypeface = fontCollection.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
+ }
- return GetTypeface(fontFamilyName, fontWeight, fontStyle);
+ return new GlyphTypefaceImpl(skTypeface);
}
}
}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index a9358cb458..8effb94ca9 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -29,7 +29,7 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
- var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
+ var glyphTypeface = (GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl;
_paint = new SKPaint
{
@@ -38,7 +38,7 @@ namespace Avalonia.Skia
IsAntialias = true,
LcdRenderText = true,
SubpixelText = true,
- Typeface = entry.SKTypeface,
+ Typeface = glyphTypeface.Typeface,
TextSize = (float)fontSize,
TextAlign = textAlignment.ToSKTextAlign()
};
diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs
new file mode 100644
index 0000000000..e0f62d6085
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs
@@ -0,0 +1,35 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ ///
+ public class GlyphRunImpl : IGlyphRunImpl
+ {
+ public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
+ {
+ Paint = paint;
+ TextBlob = textBlob;
+ }
+
+ ///
+ /// Gets the paint to draw with.
+ ///
+ public SKPaint Paint { get; }
+
+ ///
+ /// Gets the text blob to draw.
+ ///
+ public SKTextBlob TextBlob { get; }
+
+ void IDisposable.Dispose()
+ {
+ TextBlob.Dispose();
+ Paint.Dispose();
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
index e46f766255..bb2650a5c6 100644
--- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -14,9 +14,9 @@ namespace Avalonia.Skia
{
private bool _isDisposed;
- public GlyphTypefaceImpl(Typeface typeface)
+ public GlyphTypefaceImpl(SKTypeface typeface)
{
- Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
+ Typeface = typeface;
Face = new Face(GetTable)
{
@@ -61,6 +61,8 @@ namespace Avalonia.Skia
{
StrikethroughThickness = strikethroughThickness;
}
+
+ IsFixedPitch = Typeface.IsFixedPitch;
}
public Face Face { get; }
@@ -81,7 +83,6 @@ namespace Avalonia.Skia
///
public int LineGap { get; }
- //ToDo: Get these values from HarfBuzz
///
public int UnderlinePosition { get; }
@@ -94,6 +95,9 @@ namespace Avalonia.Skia
///
public int StrikethroughThickness { get; }
+ ///
+ public bool IsFixedPitch { get; }
+
///
public ushort GetGlyph(uint codepoint)
{
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index ee0cfb2f06..05c3bbdaa0 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls.Platform.Surfaces;
@@ -18,9 +17,6 @@ namespace Avalonia.Skia
///
internal class PlatformRenderInterface : IPlatformRenderInterface
{
- private readonly ConcurrentDictionary _glyphTypefaceCache =
- new ConcurrentDictionary();
-
private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; }
@@ -60,7 +56,7 @@ namespace Avalonia.Skia
Size constraint,
IReadOnlyList spans)
{
- return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
+ return new FormattedTextImpl(text, typeface, fontSize, textAlignment, wrapping, constraint, spans);
}
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
@@ -155,9 +151,95 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format);
}
- public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ ///
+ public IFontManagerImpl CreateFontManager()
{
- return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+ return new FontManagerImpl();
+ }
+
+ ///
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ {
+ var count = glyphRun.GlyphIndices.Length;
+
+ var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
+
+ var typeface = glyphTypeface.Typeface;
+
+ var paint = new SKPaint
+ {
+ TextSize = (float)glyphRun.FontRenderingEmSize,
+ Typeface = typeface,
+ TextEncoding = SKTextEncoding.GlyphId,
+ IsAntialias = true,
+ IsStroke = false,
+ SubpixelText = true
+ };
+
+ using (var textBlobBuilder = new SKTextBlobBuilder())
+ {
+ var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
+
+ if (glyphRun.GlyphOffsets.IsEmpty)
+ {
+ width = 0;
+
+ var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
+
+ if (!glyphTypeface.IsFixedPitch)
+ {
+ var positions = buffer.GetPositionSpan();
+
+ for (var i = 0; i < count; i++)
+ {
+ positions[i] = (float)width;
+
+ if (glyphRun.GlyphAdvances.IsEmpty)
+ {
+ width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+ }
+ else
+ {
+ width += glyphRun.GlyphAdvances[i];
+ }
+ }
+ }
+
+ buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
+ }
+ else
+ {
+ var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
+
+ var glyphPositions = buffer.GetPositionSpan();
+
+ var currentX = 0.0;
+
+ for (var i = 0; i < count; i++)
+ {
+ var glyphOffset = glyphRun.GlyphOffsets[i];
+
+ glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
+
+ if (glyphRun.GlyphAdvances.IsEmpty)
+ {
+ currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+ }
+ else
+ {
+ currentX += glyphRun.GlyphAdvances[i];
+ }
+ }
+
+ buffer.SetGlyphs(glyphRun.GlyphIndices.AsSpan());
+
+ width = currentX;
+ }
+
+ var textBlob = textBlobBuilder.Build();
+
+ return new GlyphRunImpl(paint, textBlob);
+ }
}
}
}
diff --git a/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..f6aabfae39
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
+[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")]
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
index 577567a8a1..d1c1961a8a 100644
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
@@ -5,58 +5,59 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
- private readonly ConcurrentDictionary> _fontFamilies =
- new ConcurrentDictionary>();
+ private readonly ConcurrentDictionary _typefaces =
+ new ConcurrentDictionary();
- public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry)
+ public void AddTypeface(FontKey key, SKTypeface typeface)
{
- if (!_fontFamilies.TryGetValue(familyName, out var fontFamily))
- {
- fontFamily = new ConcurrentDictionary();
-
- _fontFamilies.TryAdd(familyName, fontFamily);
- }
-
- fontFamily.TryAdd(key, entry);
+ _typefaces.TryAdd(key, typeface);
}
- public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle)
+ public SKTypeface Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
- var key = new FontKey(fontWeight, fontStyle);
+ var key = new FontKey(fontFamily, fontWeight, fontStyle);
- return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
- fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
- new TypefaceCollectionEntry(Typeface.Default, SkiaSharp.SKTypeface.Default);
+ return GetNearestMatch(_typefaces, key);
}
- private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key)
+ private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key)
{
- var keys = fontFamily.Keys.Where(
+ if (typefaces.ContainsKey(key))
+ {
+ return typefaces[key];
+ }
+
+ var keys = typefaces.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray();
if (!keys.Any())
{
- keys = fontFamily.Keys.Where(
+ keys = typefaces.Keys.Where(
x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
if (!keys.Any())
{
- keys = fontFamily.Keys.Where(
+ keys = typefaces.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
(x.Style >= key.Style || x.Style < key.Style)).ToArray();
}
}
- key = keys.FirstOrDefault();
+ if (keys.Length == 0)
+ {
+ return SKTypeface.Default;
+ }
- fontFamily.TryGetValue(key, out var entry);
+ key = keys[0];
- return entry;
+ return typefaces[key];
}
}
}
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
index 4bb42c7118..71edae26df 100644
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
@@ -45,13 +45,11 @@ namespace Avalonia.Skia
{
var assetStream = assetLoader.Open(asset);
- var skTypeface = SKTypeface.FromStream(assetStream);
+ var typeface = SKTypeface.FromStream(assetStream);
- var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant);
+ var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
- var entry = new TypefaceCollectionEntry(typeface, skTypeface);
-
- typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry);
+ typeFaceCollection.AddTypeface(key, typeface);
}
return typeFaceCollection;
diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs
index ce3aef755b..f16e967f42 100644
--- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs
+++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs
@@ -25,11 +25,6 @@ namespace Avalonia.Skia
AvaloniaLocator.CurrentMutable
.Bind().ToConstant(renderInterface);
-
- var fontManager = new FontManagerImpl();
-
- AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(fontManager);
}
///
diff --git a/src/Skia/Avalonia.Skia/TypefaceCache.cs b/src/Skia/Avalonia.Skia/TypefaceCache.cs
deleted file mode 100644
index 1c2b855032..0000000000
--- a/src/Skia/Avalonia.Skia/TypefaceCache.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System.Collections.Concurrent;
-using Avalonia.Media;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
- ///
- /// Cache for Skia typefaces.
- ///
- internal static class TypefaceCache
- {
- private static readonly ConcurrentDictionary> s_cache =
- new ConcurrentDictionary>();
-
- public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
- {
- if (fontFamily.Key != null)
- {
- return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
- .Get(fontFamily.Name, fontWeight, fontStyle);
- }
-
- var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary());
-
- var key = new FontKey(fontWeight, fontStyle);
-
- if (typefaceCollection.TryGetValue(key, out var entry))
- {
- return entry;
- }
-
- var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
- SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
-
- var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
-
- entry = new TypefaceCollectionEntry(typeface, skTypeface);
-
- typefaceCollection[key] = entry;
-
- return entry;
- }
- }
-}
diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
deleted file mode 100644
index ef9f889819..0000000000
--- a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using Avalonia.Media;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
- internal class TypefaceCollectionEntry
- {
- public TypefaceCollectionEntry(Typeface typeface, SKTypeface skTypeface)
- {
- Typeface = typeface;
- SKTypeface = skTypeface;
- }
- public Typeface Typeface { get; }
- public SKTypeface SKTypeface { get; }
- }
-}
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 1bda5157a5..a2bedf3190 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -11,6 +11,9 @@ using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging;
using Avalonia.Media;
using Avalonia.Platform;
+using SharpDX.DirectWrite;
+using GlyphRun = Avalonia.Media.GlyphRun;
+using TextAlignment = Avalonia.Media.TextAlignment;
namespace Avalonia
{
@@ -28,8 +31,6 @@ namespace Avalonia.Direct2D1
{
public class Direct2D1Platform : IPlatformRenderInterface
{
- private readonly ConcurrentDictionary _glyphTypefaceCache =
- new ConcurrentDictionary();
private static readonly Direct2D1Platform s_instance = new Direct2D1Platform();
public static SharpDX.Direct3D11.Device Direct3D11Device { get; private set; }
@@ -109,7 +110,6 @@ namespace Avalonia.Direct2D1
{
InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance);
- AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
@@ -194,9 +194,57 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(format, data, size, dpi, stride);
}
- public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ ///
+ public IFontManagerImpl CreateFontManager()
{
- return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+ return new FontManagerImpl();
+ }
+
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ {
+ var glyphTypeface = (GlyphTypefaceImpl)glyphRun.GlyphTypeface.PlatformImpl;
+
+ var glyphCount = glyphRun.GlyphIndices.Length;
+
+ var run = new SharpDX.DirectWrite.GlyphRun
+ {
+ FontFace = glyphTypeface.FontFace,
+ FontSize = (float)glyphRun.FontRenderingEmSize
+ };
+
+ var indices = new short[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ indices[i] = (short)glyphRun.GlyphIndices[i];
+ }
+
+ run.Indices = indices;
+
+ run.Advances = new float[glyphCount];
+
+ width = 0;
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ run.Advances[i] = (float)glyphRun.GlyphAdvances[i];
+ width += run.Advances[i];
+ }
+
+ run.Offsets = new GlyphOffset[glyphCount];
+
+ for (var i = 0; i < glyphCount; i++)
+ {
+ var offset = glyphRun.GlyphOffsets[i];
+
+ run.Offsets[i] = new GlyphOffset
+ {
+ AdvanceOffset = (float)offset.X,
+ AscenderOffset = (float)offset.Y
+ };
+ }
+
+ return new GlyphRunImpl(run);
}
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
index b455c4fbee..78bf25d607 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
@@ -24,10 +24,11 @@ namespace Avalonia.Direct2D1.Media
{
var fontFamily = typeface.FontFamily;
var fontCollection = GetOrAddFontCollection(fontFamily);
+ int index;
- foreach (var familyName in fontFamily.FamilyNames)
+ foreach (var name in fontFamily.FamilyNames)
{
- if (fontCollection.FindFamilyName(familyName, out var index))
+ if (fontCollection.FindFamilyName(name, out index))
{
return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
@@ -36,9 +37,9 @@ namespace Avalonia.Direct2D1.Media
}
}
- InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
+ InstalledFontCollection.FindFamilyName("Segoe UI", out index);
- return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
+ return InstalledFontCollection.GetFontFamily(index).GetFirstMatchingFont(
(FontWeight)typeface.Weight,
FontStretch.Normal,
(FontStyle)typeface.Style);
diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
index 628f245ae5..aa13003643 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
@@ -316,6 +316,22 @@ namespace Avalonia.Direct2D1.Media
}
}
+ ///
+ /// Draws a glyph run.
+ ///
+ /// The foreground.
+ /// The glyph run.
+ ///
+ public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
+ {
+ using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size))
+ {
+ var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
+
+ _renderTarget.DrawGlyphRun(baselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural);
+ }
+ }
+
public IRenderTargetBitmapImpl CreateLayer(Size size)
{
if (_layerFactory != null)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
index 94de397652..31604ad15f 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
+using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
@@ -14,14 +15,12 @@ namespace Avalonia.Direct2D1.Media
{
internal class FontManagerImpl : IFontManagerImpl
{
- public FontManagerImpl()
+ public string GetDefaultFontFamilyName()
{
//ToDo: Implement a real lookup of the system's default font.
- DefaultFontFamilyName = "segoe ui";
+ return "Segoe UI";
}
- public string DefaultFontFamilyName { get; }
-
public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@@ -36,17 +35,9 @@ namespace Avalonia.Direct2D1.Media
return fontFamilies;
}
- public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
+ public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
+ FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{
- //ToDo: Implement caching.
- return new Typeface(fontFamily, fontWeight, fontStyle);
- }
-
- public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
- FontFamily fontFamily = null, CultureInfo culture = null)
- {
- var fontFamilyName = FontFamily.Default.Name;
-
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
for (var i = 0; i < familyCount; i++)
@@ -60,12 +51,21 @@ namespace Avalonia.Direct2D1.Media
continue;
}
- fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
+ var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
- break;
+ fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+
+ return true;
}
- return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+ fontKey = default;
+
+ return false;
+ }
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return new GlyphTypefaceImpl(typeface);
}
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
index b1a177ad24..8e492a66ff 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
@@ -22,10 +22,16 @@ namespace Avalonia.Direct2D1.Media
{
Text = text;
- using (var font = Direct2D1FontCollectionCache.GetFont(typeface))
- using (var textFormat = new DWrite.TextFormat(Direct2D1Platform.DirectWriteFactory,
- typeface.FontFamily.Name, font.FontFamily.FontCollection, (DWrite.FontWeight)typeface.Weight,
- (DWrite.FontStyle)typeface.Style, DWrite.FontStretch.Normal, (float)fontSize))
+ var font = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).DWFont;
+ var familyName = font.FontFamily.FamilyNames.GetString(0);
+ using (var textFormat = new DWrite.TextFormat(
+ Direct2D1Platform.DirectWriteFactory,
+ familyName,
+ font.FontFamily.FontCollection,
+ (DWrite.FontWeight)typeface.Weight,
+ (DWrite.FontStyle)typeface.Style,
+ DWrite.FontStretch.Normal,
+ (float)fontSize))
{
textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
new file mode 100644
index 0000000000..0b06d5ef3e
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
@@ -0,0 +1,19 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Direct2D1.Media
+{
+ internal class GlyphRunImpl : IGlyphRunImpl
+ {
+ public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun)
+ {
+ GlyphRun = glyphRun;
+ }
+
+ public SharpDX.DirectWrite.GlyphRun GlyphRun { get; }
+
+ public void Dispose()
+ {
+ GlyphRun?.Dispose();
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
index 32def01c39..dfc3b48eaa 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Direct2D1.Media
{
DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
- FontFace = new FontFace(DWFont);
+ FontFace = new FontFace(DWFont).QueryInterface();
Face = new Face(GetTable);
@@ -59,6 +59,8 @@ namespace Avalonia.Direct2D1.Media
{
StrikethroughThickness = strikethroughThickness;
}
+
+ IsFixedPitch = FontFace.IsMonospacedFont;
}
private Blob GetTable(Face face, Tag tag)
@@ -82,7 +84,7 @@ namespace Avalonia.Direct2D1.Media
public SharpDX.DirectWrite.Font DWFont { get; }
- public FontFace FontFace { get; }
+ public FontFace1 FontFace { get; }
public Face Face { get; }
@@ -113,6 +115,9 @@ namespace Avalonia.Direct2D1.Media
///
public int StrikethroughThickness { get; }
+ ///
+ public bool IsFixedPitch { get; }
+
///
public ushort GetGlyph(uint codepoint)
{
diff --git a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
index a09d5c2d1c..26a8526c16 100644
--- a/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
+++ b/src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
@@ -2,9 +2,13 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Reflection;
+using System.Runtime.CompilerServices;
using Avalonia.Platform;
using Avalonia.Direct2D1;
[assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize),
typeof(Direct2DChecker))]
+[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
+[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")]
+
diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
index df2508a3ed..6e87a90ea4 100644
--- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
@@ -4,6 +4,7 @@
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
@@ -245,6 +246,23 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(false, item.IsSelected);
}
+ [Fact]
+ public void Can_Decrease_Number_Of_Materialized_Items_By_Removing_From_Source_Collection()
+ {
+ var items = new AvaloniaList(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
+ var target = new ListBox
+ {
+ Template = ListBoxTemplate(),
+ Items = items,
+ ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 })
+ };
+
+ Prepare(target);
+ target.Scroll.Offset = new Vector(0, 1);
+
+ items.RemoveRange(0, 11);
+ }
+
private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
{
_mouse.Click(listBox, item, mouseButton);
@@ -383,14 +401,26 @@ namespace Avalonia.Controls.UnitTests
private FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate((parent, scope) =>
- new ScrollContentPresenter
+ new Panel
{
- Name = "PART_ContentPresenter",
- [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
- [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
- [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
- [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
- }.RegisterInNameScope(scope));
+ Children =
+ {
+ new ScrollContentPresenter
+ {
+ Name = "PART_ContentPresenter",
+ [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
+ [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
+ [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
+ [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
+ }.RegisterInNameScope(scope),
+ new ScrollBar
+ {
+ Name = "verticalScrollBar",
+ [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty],
+ [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty],
+ }
+ }
+ });
}
private void Prepare(ListBox target)
diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
new file mode 100644
index 0000000000..82471915f4
--- /dev/null
+++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Reflection;
+using Avalonia.Direct2D1.Media;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Direct2D1.UnitTests.Media
+{
+ public class FontManagerImplTests
+ {
+ private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono";
+
+ [Fact]
+ public void Should_Create_Typeface_From_Fallback()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ Direct2D1Platform.Initialize();
+
+ var fontManager = new FontManagerImpl();
+
+ var defaultName = fontManager.GetDefaultFontFamilyName();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily("A, B, Arial")));
+
+ var font = glyphTypeface.DWFont;
+
+ Assert.Equal("Arial", font.FontFamily.FamilyNames.GetString(0));
+
+ Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
+
+ Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
+ }
+ }
+
+ [Fact]
+ public void Should_Create_Typeface_For_Unknown_Font()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ Direct2D1Platform.Initialize();
+
+ var fontManager = new FontManagerImpl();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily("Unknown")));
+
+ var font = glyphTypeface.DWFont;
+
+ var defaultName = fontManager.GetDefaultFontFamilyName();
+
+ Assert.Equal(defaultName, font.FontFamily.FamilyNames.GetString(0));
+
+ Assert.Equal(SharpDX.DirectWrite.FontWeight.Normal, font.Weight);
+
+ Assert.Equal(SharpDX.DirectWrite.FontStyle.Normal, font.Style);
+ }
+ }
+
+ [Fact]
+ public void Should_Load_Typeface_From_Resource()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ Direct2D1Platform.Initialize();
+
+ var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
+
+ var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
+
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader);
+
+ var fontManager = new FontManagerImpl();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily(s_fontUri)));
+
+ var font = glyphTypeface.DWFont;
+
+ Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
+ }
+ }
+
+ [Fact]
+ public void Should_Load_Nearest_Matching_Font()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ Direct2D1Platform.Initialize();
+
+ var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
+
+ var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
+
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader);
+
+ var fontManager = new FontManagerImpl();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic));
+
+ var font = glyphTypeface.DWFont;
+
+ Assert.Equal("Noto Mono", font.FontFamily.FamilyNames.GetString(0));
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
index a683e5cfca..f063d59ca4 100644
--- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
+++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
@@ -182,6 +182,8 @@ namespace Avalonia.Layout.UnitTests
It.IsAny>()))
.Returns(new FormattedTextMock("TEST"));
+ renderInterface.Setup(x => x.CreateFontManager()).Returns(new MockFontManagerImpl());
+
var streamGeometry = new Mock();
streamGeometry.Setup(x =>
x.Open())
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index f4d4a9dd2a..eaf9f22406 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -307,6 +307,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("child", child.Name);
}
+ [Fact]
+ public void ControlTemplate_With_TargetType_Is_Operational()
+ {
+ var xaml = @"
+
+
+
+";
+ var template = AvaloniaXamlLoader.Parse(xaml);
+
+ Assert.Equal(typeof(ContentControl), template.TargetType);
+
+ Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control);
+ }
+
[Fact]
public void ControlTemplate_With_Panel_Children_Are_Added()
{
@@ -704,11 +721,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
-
- [Fact(Skip =
-@"Doesn't work with Portable.xaml, it's working in different creation order -
-Handled in test 'Control_Is_Added_To_Parent_Before_Final_EndInit'
-do we need it?")]
+ [Fact]
public void Control_Is_Added_To_Parent_Before_Properties_Are_Set()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
index b76022852c..f7629e5b9e 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
@@ -5,6 +5,7 @@ using System.Xml;
using Avalonia.Controls;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
@@ -38,6 +39,57 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
+ [Fact]
+ public void DataTemplate_Can_Be_Added_To_Style_Resources()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
+ {
+ var xaml = @"
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var userControl = (UserControl)loader.Load(xaml);
+ var dataTemplate = (DataTemplate)((Style)userControl.Styles[0]).Resources["dataTemplate"];
+
+ Assert.NotNull(dataTemplate);
+ }
+ }
+
+ [Fact]
+ public void ControlTemplate_Can_Be_Added_To_Style_Resources()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
+ {
+ var xaml = @"
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var userControl = (UserControl)loader.Load(xaml);
+ var controlTemplate = (ControlTemplate)((Style)userControl.Styles[0]).Resources["controlTemplate"];
+
+ Assert.NotNull(controlTemplate);
+ Assert.Equal(typeof(Button), controlTemplate.TargetType);
+ }
+ }
+
[Fact]
public void SolidColorBrush_Can_Be_Added_To_Style_Resources()
{
diff --git a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
index bca34dd69d..73e63ae2ac 100644
--- a/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
+++ b/tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
@@ -53,7 +53,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{
var r = AvaloniaLocator.Current.GetService();
return r.CreateFormattedText(text,
- new Typeface(fontFamily, fontWeight, fontStyle),
+ FontManager.Current.GetOrAddTypeface(fontFamily, fontWeight, fontStyle),
fontSize,
textAlignment,
wrapping,
diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
new file mode 100644
index 0000000000..927f98b32b
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Reflection;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using SkiaSharp;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests
+{
+ public class FontManagerImplTests
+ {
+ private static string s_fontUri = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono";
+
+ [Fact]
+ public void Should_Create_Typeface_From_Fallback()
+ {
+ var fontManager = new FontManagerImpl();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily("A, B, " + fontManager.GetDefaultFontFamilyName())));
+
+ var skTypeface = glyphTypeface.Typeface;
+
+ Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
+
+ Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
+
+ Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
+ }
+
+ [Fact]
+ public void Should_Create_Typeface_For_Unknown_Font()
+ {
+ var fontManager = new FontManagerImpl();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily("Unknown")));
+
+ var skTypeface = glyphTypeface.Typeface;
+
+ Assert.Equal(SKTypeface.Default.FamilyName, skTypeface.FamilyName);
+
+ Assert.Equal(SKTypeface.Default.FontWeight, skTypeface.FontWeight);
+
+ Assert.Equal(SKTypeface.Default.FontSlant, skTypeface.FontSlant);
+ }
+
+ [Fact]
+ public void Should_Load_Typeface_From_Resource()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
+
+ var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
+
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader);
+
+ var fontManager = new FontManagerImpl();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily(s_fontUri)));
+
+ var skTypeface = glyphTypeface.Typeface;
+
+ Assert.Equal("Noto Mono", skTypeface.FamilyName);
+ }
+ }
+
+ [Fact]
+ public void Should_Load_Nearest_Matching_Font()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ var assetLoaderType = typeof(TestRoot).Assembly.GetType("Avalonia.Shared.PlatformSupport.AssetLoader");
+
+ var assetLoader = (IAssetLoader)Activator.CreateInstance(assetLoaderType, (Assembly)null);
+
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(assetLoader);
+
+ var fontManager = new FontManagerImpl();
+
+ var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface(
+ new Typeface(new FontFamily(s_fontUri), FontWeight.Black, FontStyle.Italic));
+
+ var skTypeface = glyphTypeface.Typeface;
+
+ Assert.Equal("Noto Mono", skTypeface.FamilyName);
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf
new file mode 100644
index 0000000000..3560a3a0c8
Binary files /dev/null and b/tests/Avalonia.UnitTests/Assets/NotoMono-Regular.ttf differ
diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
index 272b1fc489..b1d89037da 100644
--- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
+++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
@@ -7,6 +7,9 @@
false
latest
+
+
+
@@ -20,7 +23,7 @@
+
-
diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs
new file mode 100644
index 0000000000..faf6f98138
--- /dev/null
+++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Platform;
+using Moq;
+
+namespace Avalonia.UnitTests
+{
+ public class MockFontManagerImpl : IFontManagerImpl
+ {
+ public string GetDefaultFontFamilyName()
+ {
+ return "Default";
+ }
+
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ {
+ return new[] { "Default" };
+ }
+
+ public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
+ CultureInfo culture, out FontKey fontKey)
+ {
+ fontKey = default;
+
+ return false;
+ }
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return Mock.Of();
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockGlyphTypeface.cs b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs
new file mode 100644
index 0000000000..93ff84d04a
--- /dev/null
+++ b/tests/Avalonia.UnitTests/MockGlyphTypeface.cs
@@ -0,0 +1,47 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+ public class MockGlyphTypeface : IGlyphTypefaceImpl
+ {
+ public short DesignEmHeight => 10;
+ public int Ascent => 100;
+ public int Descent => 0;
+ public int LineGap { get; }
+ public int UnderlinePosition { get; }
+ public int UnderlineThickness { get; }
+ public int StrikethroughPosition { get; }
+ public int StrikethroughThickness { get; }
+ public bool IsFixedPitch { get; }
+
+ public ushort GetGlyph(uint codepoint)
+ {
+ return 0;
+ }
+
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ return new ushort[codepoints.Length];
+ }
+
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return 100;
+ }
+
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var advances = new int[glyphs.Length];
+
+ for (var i = 0; i < advances.Length; i++)
+ {
+ advances[i] = 100;
+ }
+
+ return advances;
+ }
+
+ public void Dispose() { }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 187853283f..5da9f8ff6e 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -79,9 +79,15 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
- public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ public IFontManagerImpl CreateFontManager()
{
- return Mock.Of();
+ return new MockFontManagerImpl();
+ }
+
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ {
+ width = 0;
+ return Mock.Of();
}
}
}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
new file mode 100644
index 0000000000..6cbab08905
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
@@ -0,0 +1,25 @@
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class FontManagerTests
+ {
+ [Fact]
+ public void Should_Create_Single_Instance_Typeface()
+ {
+ using (AvaloniaLocator.EnterScope())
+ {
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(new MockPlatformRenderInterface());
+
+ var fontFamily = new FontFamily("MyFont");
+
+ var typeface = FontManager.Current.GetOrAddTypeface(fontFamily);
+
+ Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
new file mode 100644
index 0000000000..f5e4cdc099
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs
@@ -0,0 +1,130 @@
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+ public class GlyphRunTests : TestWithServicesBase
+ {
+ public GlyphRunTests()
+ {
+ AvaloniaLocator.CurrentMutable
+ .Bind().ToSingleton();
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 3, 30)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 1, 0, 10)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 0, 20)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 2, 1, 30)]
+ [Theory]
+ public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters))
+ {
+ var characterHit = new CharacterHit(start, trailingLength);
+
+ var distance = glyphRun.GetDistanceFromCharacterHit(characterHit);
+
+ Assert.Equal(expectedDistance, distance);
+ }
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 25.0, 0, 3, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 20.0, 2, 0, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 26.0, 2, 1, true)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 35.0, 2, 1, false)]
+ [Theory]
+ public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
+ int trailingLengthExpected, bool isInsideExpected)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters))
+ {
+ var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
+
+ Assert.Equal(start, textBounds.FirstCharacterIndex);
+
+ Assert.Equal(trailingLengthExpected, textBounds.TrailingLength);
+
+ Assert.Equal(isInsideExpected, isInside);
+ }
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 30.0)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 1, 2 }, 0, 1, 1, 1, 10.0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 2, 1, 2, 20.0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 3 }, 0, 1, 1, 2, 20.0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 1, 1, 0 }, 1, 1, 1, 2, 20.0)]
+ [Theory]
+ public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
+ int index, int expectedIndex, int expectedLength, double expectedWidth)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
+
+ Assert.Equal(expectedIndex, textBounds.FirstCharacterIndex);
+
+ Assert.Equal(expectedLength, textBounds.TrailingLength);
+
+ Assert.Equal(expectedWidth, width, 2);
+ }
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 0)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 3, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 0, 3, 1, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 0, 3, 1, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 0, 4, 1, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 0, 4, 1, 1)]
+ [Theory]
+ public void Should_Get_Next_CharacterHit(double[] advances, ushort[] clusters,
+ int currentIndex, int currentLength,
+ int nextIndex, int nextLength,
+ int bidiLevel)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
+
+ Assert.Equal(nextIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(nextLength, characterHit.TrailingLength);
+ }
+ }
+
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 0)]
+ [InlineData(new double[] { 10, 10, 10 }, new ushort[] { 0, 0, 0 }, 0, 0, 0, 0, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 0, 0, 0, 3 }, 3, 1, 3, 0, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10 }, new ushort[] { 3, 0, 0, 0 }, 3, 1, 3, 0, 1)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 0, 1, 1, 1, 4 }, 4, 1, 4, 0, 0)]
+ [InlineData(new double[] { 10, 10, 10, 10, 10 }, new ushort[] { 4, 1, 1, 1, 0 }, 4, 1, 4, 0, 1)]
+ [Theory]
+ public void Should_Get_Previous_CharacterHit(double[] advances, ushort[] clusters,
+ int currentIndex, int currentLength,
+ int previousIndex, int previousLength,
+ int bidiLevel)
+ {
+ using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
+ {
+ var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
+
+ Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);
+
+ Assert.Equal(previousLength, characterHit.TrailingLength);
+ }
+ }
+
+ private static GlyphRun CreateGlyphRun(double[] glyphAdvances, ushort[] glyphClusters, int bidiLevel = 0)
+ {
+ var count = glyphAdvances.Length;
+ var glyphIndices = new ushort[count];
+
+ var bounds = new Rect(0, 0, count * 10, 10);
+
+ return new GlyphRun(new GlyphTypeface(new MockGlyphTypeface()), 10, glyphIndices, glyphAdvances,
+ glyphClusters: glyphClusters, bidiLevel: bidiLevel, bounds: bounds);
+ }
+ }
+}
diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
index 032b6582a9..28304b674b 100644
--- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
@@ -3,13 +3,12 @@ using System.Collections.Generic;
using System.IO;
using Avalonia.Media;
using Avalonia.Platform;
+using Avalonia.UnitTests;
namespace Avalonia.Visuals.UnitTests.VisualTree
{
class MockRenderInterface : IPlatformRenderInterface
{
- public IEnumerable InstalledFontNames => new string[0];
-
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
@@ -52,11 +51,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
throw new NotImplementedException();
}
+ public IFontManagerImpl CreateFontManager()
+ {
+ return new MockFontManagerImpl();
+ }
+
public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? fmt)
{
throw new NotImplementedException();