diff --git a/Avalonia.sln b/Avalonia.sln
index 568a16ce0e..e40ebae4d6 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -128,6 +128,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
build\BuildTargets.targets = build\BuildTargets.targets
+ build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props
@@ -201,7 +202,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..dc57a73f48
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at steven@avaloniaui.net. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 92e4afdca8..603308ef9a 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -52,7 +52,7 @@ jobs:
sdk: 'macosx10.14'
configuration: 'Release'
xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
- xcodeVersion: 'default' # Options: 8, 9, default, specifyPath
+ xcodeVersion: '10' # Options: 8, 9, default, specifyPath
args: '-derivedDataPath ./'
- task: CmdLine@2
diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
new file mode 100644
index 0000000000..f8767c7599
--- /dev/null
+++ b/build/HarfBuzzSharp.props
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index c03ad0fefd..796bd8e596 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs
index eb38a66a84..8523b9537d 100644
--- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs
@@ -10,3 +10,4 @@ using System.Runtime.CompilerServices;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]
[assembly: InternalsVisibleTo("Avalonia.LeakTests")]
+[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")]
diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs
index 10ea6bf523..a69ad50a4b 100644
--- a/src/Avalonia.Animation/TransitionInstance.cs
+++ b/src/Avalonia.Animation/TransitionInstance.cs
@@ -28,7 +28,7 @@ namespace Avalonia.Animation
private void TimerTick(TimeSpan t)
{
- var interpVal = (double)t.Ticks / _duration.Ticks;
+ var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks;
// Clamp interpolation value.
if (interpVal >= 1d | interpVal < 0d)
diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs
new file mode 100644
index 0000000000..a8306c2d69
--- /dev/null
+++ b/src/Avalonia.Base/EnumExtensions.cs
@@ -0,0 +1,23 @@
+// 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.Runtime.CompilerServices;
+
+namespace Avalonia
+{
+ ///
+ /// Provides extension methods for enums.
+ ///
+ public static class EnumExtensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum
+ {
+ var intValue = *(int*)&value;
+ var intFlag = *(int*)&flag;
+
+ return (intValue & intFlag) == intFlag;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index 41b57b6e70..027028480c 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/src/Avalonia.Base/Utilities/MathUtilities.cs
@@ -159,6 +159,11 @@ namespace Avalonia.Utilities
/// The clamped value.
public static int Clamp(int val, int min, int max)
{
+ if (min > max)
+ {
+ throw new ArgumentException($"{min} cannot be greater than {max}.");
+ }
+
if (val < min)
{
return min;
diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs
index 1bdbd4ca7c..e06c5996c9 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/src/Avalonia.Base/ValueStore.cs
@@ -57,7 +57,8 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
- _propertyValues.SetValue(property, Validate(property, value));
+ Validate(property, ref value);
+ _propertyValues.SetValue(property, value);
Changed(property, priority, v, value);
return;
}
@@ -78,7 +79,8 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
- _propertyValues.AddValue(property, Validate(property, value));
+ Validate(property, ref value);
+ _propertyValues.AddValue(property, value);
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
@@ -166,16 +168,14 @@ namespace Avalonia
validate2);
}
- private object Validate(AvaloniaProperty property, object value)
+ private void Validate(AvaloniaProperty property, ref object value)
{
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType());
if (validate != null && value != AvaloniaProperty.UnsetValue)
{
- return validate(_owner, value);
+ value = validate(_owner, value);
}
-
- return value;
}
private DeferredSetter GetDeferredSetter(AvaloniaProperty property)
diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs
index d9be9171ed..3b644191c2 100644
--- a/src/Avalonia.Controls/AppBuilderBase.cs
+++ b/src/Avalonia.Controls/AppBuilderBase.cs
@@ -125,9 +125,8 @@ namespace Avalonia.Controls
});
// Copy-pasted because we can't call extension methods due to generic constraints
- var lifetime = new ClassicDesktopStyleApplicationLifetime(Instance) {ShutdownMode = ShutdownMode.OnMainWindowClose};
- Instance.ApplicationLifetime = lifetime;
- SetupWithoutStarting();
+ var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = ShutdownMode.OnMainWindowClose};
+ SetupWithLifetime(lifetime);
lifetime.Start(Array.Empty());
}
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
index 2533191ae4..6dd5b8cc81 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
@@ -5,12 +5,12 @@ using System.Threading;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
+using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes
{
public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable
{
- private readonly Application _app;
private int _exitCode;
private CancellationTokenSource _cts;
private bool _isShuttingDown;
@@ -34,12 +34,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
_activeLifetime?._windows.Add((Window)sender);
}
- public ClassicDesktopStyleApplicationLifetime(Application app)
+ public ClassicDesktopStyleApplicationLifetime()
{
if (_activeLifetime != null)
throw new InvalidOperationException(
"Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed");
- _app = app;
_activeLifetime = this;
}
@@ -103,7 +102,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
_cts = new CancellationTokenSource();
MainWindow?.Show();
- _app.Run(_cts.Token);
+ Dispatcher.UIThread.MainLoop(_cts.Token);
Environment.ExitCode = _exitCode;
return _exitCode;
}
@@ -124,7 +123,7 @@ namespace Avalonia
this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
where T : AppBuilderBase, new()
{
- var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode};
+ var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode};
builder.SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index ce4358648b..64db832a81 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -704,7 +704,7 @@ namespace Avalonia.Controls
added.Add(e.NewValue);
}
- OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
+ OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed));
}
///
diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs
index 9c520c434e..293b6326d6 100644
--- a/src/Avalonia.Controls/ColumnDefinition.cs
+++ b/src/Avalonia.Controls/ColumnDefinition.cs
@@ -26,6 +26,16 @@ namespace Avalonia.Controls
public static readonly StyledProperty WidthProperty =
AvaloniaProperty.Register(nameof(Width), new GridLength(1, GridUnitType.Star));
+ ///
+ /// Initializes static members of the class.
+ ///
+ static ColumnDefinition()
+ {
+ AffectsParentMeasure(MinWidthProperty, MaxWidthProperty);
+
+ WidthProperty.Changed.AddClassHandler(OnUserSizePropertyChanged);
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -68,7 +78,6 @@ namespace Avalonia.Controls
}
set
{
- Parent?.InvalidateMeasure();
SetValue(MaxWidthProperty, value);
}
}
@@ -84,7 +93,6 @@ namespace Avalonia.Controls
}
set
{
- Parent?.InvalidateMeasure();
SetValue(MinWidthProperty, value);
}
}
@@ -100,7 +108,6 @@ namespace Avalonia.Controls
}
set
{
- Parent?.InvalidateMeasure();
SetValue(WidthProperty, value);
}
}
diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs
index a68fe1265f..e4ae777453 100644
--- a/src/Avalonia.Controls/DefinitionBase.cs
+++ b/src/Avalonia.Controls/DefinitionBase.cs
@@ -7,9 +7,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
-
-using Avalonia;
-using Avalonia.Collections;
using Avalonia.Utilities;
namespace Avalonia.Controls
@@ -50,6 +47,8 @@ namespace Avalonia.Controls
}
}
}
+
+ Parent?.InvalidateMeasure();
}
///
@@ -63,6 +62,8 @@ namespace Avalonia.Controls
_sharedState.RemoveMember(this);
_sharedState = null;
}
+
+ Parent?.InvalidateMeasure();
}
///
@@ -114,6 +115,36 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Notifies parent or size scope that definition size has been changed.
+ ///
+ internal static void OnUserSizePropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (definition.Parent == null)
+ {
+ return;
+ }
+
+ if (definition._sharedState != null)
+ {
+ definition._sharedState.Invalidate();
+ }
+ else
+ {
+ GridUnitType oldUnitType = ((GridLength)e.OldValue).GridUnitType;
+ GridUnitType newUnitType = ((GridLength)e.NewValue).GridUnitType;
+
+ if (oldUnitType != newUnitType)
+ {
+ definition.Parent.Invalidate();
+ }
+ else
+ {
+ definition.Parent.InvalidateMeasure();
+ }
+ }
+ }
+
///
/// Returns true if this definition is a part of shared group.
///
@@ -730,5 +761,22 @@ namespace Avalonia.Controls
SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged);
PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged);
}
+
+ ///
+ /// Marks a property on a definition as affecting the parent grid's measurement.
+ ///
+ /// The properties.
+ protected static void AffectsParentMeasure(params AvaloniaProperty[] properties)
+ {
+ void Invalidate(AvaloniaPropertyChangedEventArgs e)
+ {
+ (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure();
+ }
+
+ foreach (var property in properties)
+ {
+ property.Changed.Subscribe(Invalidate);
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/DesktopApplicationExtensions.cs b/src/Avalonia.Controls/DesktopApplicationExtensions.cs
index ff6705cdc0..ddd4e57a40 100644
--- a/src/Avalonia.Controls/DesktopApplicationExtensions.cs
+++ b/src/Avalonia.Controls/DesktopApplicationExtensions.cs
@@ -50,6 +50,7 @@ namespace Avalonia.Controls
/// On desktop-style platforms runs the application's main loop with custom CancellationToken
/// without setting a lifetime.
///
+ /// The application.
/// The token to track.
public static void Run(this Application app, CancellationToken token)
{
diff --git a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
index a6a64e570b..d99648a158 100644
--- a/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
@@ -48,11 +48,6 @@ namespace Avalonia.Controls.Generators
tabItem[~ContentControl.ContentTemplateProperty] = Owner[~TabControl.ContentTemplateProperty];
}
- if (tabItem.Content == null)
- {
- tabItem[~ContentControl.ContentProperty] = tabItem[~StyledElement.DataContextProperty];
- }
-
return tabItem;
}
}
diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs
index 8ecfe349f8..23c1cd4794 100644
--- a/src/Avalonia.Controls/Grid.cs
+++ b/src/Avalonia.Controls/Grid.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Threading;
@@ -178,6 +179,7 @@ namespace Avalonia.Controls
if (_data == null) { _data = new ExtendedData(); }
_data.ColumnDefinitions = value;
_data.ColumnDefinitions.Parent = this;
+ InvalidateMeasure();
}
}
@@ -198,6 +200,7 @@ namespace Avalonia.Controls
if (_data == null) { _data = new ExtendedData(); }
_data.RowDefinitions = value;
_data.RowDefinitions.Parent = this;
+ InvalidateMeasure();
}
}
@@ -569,6 +572,15 @@ namespace Avalonia.Controls
return (arrangeSize);
}
+ ///
+ ///
+ ///
+ protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ CellsStructureDirty = true;
+ base.ChildrenChanged(sender, e);
+ }
+
///
/// Invalidates grid caches and makes the grid dirty for measure.
///
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index b027da6d0c..bf22f0a08a 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -359,6 +359,12 @@ namespace Avalonia.Controls
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
+
+ if (Presenter != null)
+ {
+ Presenter.Items = newValue;
+ }
+
SubscribeToItems(newValue);
}
@@ -370,6 +376,8 @@ namespace Avalonia.Controls
/// The event args.
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
+ UpdateItemCount();
+
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@@ -381,7 +389,7 @@ namespace Avalonia.Controls
break;
}
- UpdateItemCount();
+ Presenter?.ItemsChanged(e);
var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection == null || collection.Count == 0);
diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
index 42311dc781..c4acf1ebef 100644
--- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
@@ -1,12 +1,19 @@
// 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;
+using System.Collections.Specialized;
+
namespace Avalonia.Controls.Presenters
{
public interface IItemsPresenter : IPresenter
{
+ IEnumerable Items { get; set; }
+
IPanel Panel { get; }
+ void ItemsChanged(NotifyCollectionChangedEventArgs e);
+
void ScrollIntoView(object item);
}
}
diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
index 0f0cdc37cf..ef1f277162 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
@@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters
_itemsSubscription?.Dispose();
_itemsSubscription = null;
- if (_createdPanel && value is INotifyCollectionChanged incc)
+ if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc)
{
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
}
@@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters
private set;
}
+ protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
+
///
public override sealed void ApplyTemplate()
{
@@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters
{
}
+ ///
+ void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e)
+ {
+ if (Panel != null)
+ {
+ ItemsChanged(e);
+ }
+ }
+
///
/// Creates the for the control.
///
@@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters
_createdPanel = true;
- if (_itemsSubscription == null && Items is INotifyCollectionChanged incc)
+ if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc)
{
_itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 5931fec350..e0cc9aa128 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -297,7 +297,8 @@ namespace Avalonia.Controls.Presenters
return new FormattedText
{
Text = "X",
- Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+ Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
+ FontSize = FontSize,
TextAlignment = TextAlignment,
Constraint = availableSize,
}.Bounds.Size;
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index 74a6d288f4..caaf58b25b 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -41,6 +41,8 @@ namespace Avalonia.Controls.Primitives
///
/// Initializes a new instance of the class.
///
+ /// The popup parent.
+ /// The popup implementation.
///
/// The dependency resolver to use. If null the default dependency resolver will be used.
///
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 7fddee1012..b6ae567123 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives
///
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- base.ItemsCollectionChanged(sender, e);
-
if (_updateCount > 0)
{
+ base.ItemsCollectionChanged(sender, e);
return;
}
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
+ break;
+ case NotifyCollectionChangedAction.Remove:
+ _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
+ break;
+ }
+
+ base.ItemsCollectionChanged(sender, e);
+
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives
}
else
{
- _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
UpdateSelectedItem(_selection.First(), false);
}
break;
case NotifyCollectionChangedAction.Remove:
- _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
UpdateSelectedItem(_selection.First(), false);
ResetSelectedItems();
break;
@@ -1088,9 +1097,15 @@ namespace Avalonia.Controls.Primitives
}
else
{
- SelectedIndex = _updateSelectedIndex != int.MinValue ?
- _updateSelectedIndex :
- AlwaysSelected ? 0 : -1;
+ if (_updateSelectedIndex != int.MinValue)
+ {
+ SelectedIndex = _updateSelectedIndex;
+ }
+
+ if (AlwaysSelected && SelectedIndex == -1)
+ {
+ SelectedIndex = 0;
+ }
}
}
}
diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs
index 1f2f738670..85e7ed6519 100644
--- a/src/Avalonia.Controls/RowDefinition.cs
+++ b/src/Avalonia.Controls/RowDefinition.cs
@@ -26,6 +26,16 @@ namespace Avalonia.Controls
public static readonly StyledProperty HeightProperty =
AvaloniaProperty.Register(nameof(Height), new GridLength(1, GridUnitType.Star));
+ ///
+ /// Initializes static members of the class.
+ ///
+ static RowDefinition()
+ {
+ AffectsParentMeasure(MaxHeightProperty, MinHeightProperty);
+
+ HeightProperty.Changed.AddClassHandler(OnUserSizePropertyChanged);
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -68,7 +78,6 @@ namespace Avalonia.Controls
}
set
{
- Parent?.InvalidateMeasure();
SetValue(MaxHeightProperty, value);
}
}
@@ -84,7 +93,6 @@ namespace Avalonia.Controls
}
set
{
- Parent?.InvalidateMeasure();
SetValue(MinHeightProperty, value);
}
}
@@ -100,7 +108,6 @@ namespace Avalonia.Controls
}
set
{
- Parent?.InvalidateMeasure();
SetValue(HeightProperty, value);
}
}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index b9603b91ed..c7855ddfd1 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -352,10 +352,11 @@ namespace Avalonia.Controls
return new FormattedText
{
Constraint = constraint,
- Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+ Typeface = new Typeface(FontFamily, FontWeight, FontStyle),
+ FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,
- Wrapping = TextWrapping,
+ TextWrapping = TextWrapping,
};
}
diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs
index 384a9db0cf..d90729e8a5 100644
--- a/src/Avalonia.Controls/ToolTipService.cs
+++ b/src/Avalonia.Controls/ToolTipService.cs
@@ -1,6 +1,7 @@
using System;
using Avalonia.Input;
using Avalonia.Threading;
+using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -79,7 +80,10 @@ namespace Avalonia.Controls
{
StopTimer();
- ToolTip.SetIsOpen(control, true);
+ if ((control as IVisual).IsAttachedToVisualTree)
+ {
+ ToolTip.SetIsOpen(control, true);
+ }
}
private void Close(Control control)
diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs
index 7c88401615..d06a71a9f8 100644
--- a/src/Avalonia.Controls/WrapPanel.cs
+++ b/src/Avalonia.Controls/WrapPanel.cs
@@ -42,7 +42,7 @@ namespace Avalonia.Controls
///
static WrapPanel()
{
- AffectsMeasure(OrientationProperty);
+ AffectsMeasure(OrientationProperty, ItemWidthProperty, ItemHeightProperty);
}
///
diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs
index c84596b913..4dcf0eee53 100644
--- a/src/Avalonia.Input/MouseDevice.cs
+++ b/src/Avalonia.Input/MouseDevice.cs
@@ -14,13 +14,14 @@ namespace Avalonia.Input
///
/// Represents a mouse device.
///
- public class MouseDevice : IMouseDevice
+ public class MouseDevice : IMouseDevice, IDisposable
{
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
private readonly Pointer _pointer;
+ private bool _disposed;
public MouseDevice(Pointer pointer = null)
{
@@ -126,7 +127,9 @@ namespace Avalonia.Input
{
Contract.Requires(e != null);
- var mouse = (IMouseDevice)e.Device;
+ var mouse = (MouseDevice)e.Device;
+ if(mouse._disposed)
+ return;
Position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e);
@@ -441,5 +444,11 @@ namespace Avalonia.Input
el = (IInputElement)el.VisualParent;
}
}
+
+ public void Dispose()
+ {
+ _disposed = true;
+ _pointer?.Dispose();
+ }
}
}
diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs
index 80d803abb1..819d231b31 100644
--- a/src/Avalonia.Input/Pointer.cs
+++ b/src/Avalonia.Input/Pointer.cs
@@ -37,7 +37,7 @@ namespace Avalonia.Input
{
if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
- var oldCapture = control;
+ var oldCapture = Captured;
Captured = control;
PlatformCapture(control);
if (oldCapture != null)
diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs
index e9f3c02b7f..1068a0d4d4 100644
--- a/src/Avalonia.Input/PointerPoint.cs
+++ b/src/Avalonia.Input/PointerPoint.cs
@@ -1,3 +1,6 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
namespace Avalonia.Input
{
public sealed class PointerPoint
@@ -27,9 +30,9 @@ namespace Avalonia.Input
public PointerPointProperties(RawInputModifiers modifiers, PointerUpdateKind kind)
{
PointerUpdateKind = kind;
- IsLeftButtonPressed = modifiers.HasFlag(RawInputModifiers.LeftMouseButton);
- IsMiddleButtonPressed = modifiers.HasFlag(RawInputModifiers.MiddleMouseButton);
- IsRightButtonPressed = modifiers.HasFlag(RawInputModifiers.RightMouseButton);
+ IsLeftButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.LeftMouseButton);
+ IsMiddleButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.MiddleMouseButton);
+ IsRightButtonPressed = modifiers.HasFlagCustom(RawInputModifiers.RightMouseButton);
// The underlying input source might be reporting the previous state,
// so make sure that we reflect the current state
diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs
index b231c9fff4..d6ad836f37 100644
--- a/src/Avalonia.Input/TouchDevice.cs
+++ b/src/Avalonia.Input/TouchDevice.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
@@ -11,10 +12,11 @@ namespace Avalonia.Input
/// This class is supposed to be used on per-toplevel basis, don't use a shared one
///
///
- public class TouchDevice : IInputDevice
+ public class TouchDevice : IInputDevice, IDisposable
{
- Dictionary _pointers = new Dictionary();
-
+ private readonly Dictionary _pointers = new Dictionary();
+ private bool _disposed;
+
KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) =>
(KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
@@ -28,6 +30,8 @@ namespace Avalonia.Input
public void ProcessRawEvent(RawInputEventArgs ev)
{
+ if(_disposed)
+ return;
var args = (RawTouchEventArgs)ev;
if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
{
@@ -82,6 +86,17 @@ namespace Avalonia.Input
}
+
+ public void Dispose()
+ {
+ if(_disposed)
+ return;
+ var values = _pointers.Values.ToList();
+ _pointers.Clear();
+ _disposed = true;
+ foreach (var p in values)
+ p.Dispose();
+ }
}
}
diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs
index ddb71b61bb..fab3ce36b8 100644
--- a/src/Avalonia.Native/AvaloniaNativePlatform.cs
+++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs
@@ -95,7 +95,6 @@ namespace Avalonia.Native
.Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
.Bind().ToSingleton()
.Bind().ToConstant(KeyboardDevice)
- .Bind().ToConstant(MouseDevice)
.Bind().ToConstant(this)
.Bind().ToConstant(this)
.Bind().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs
index fe7458d583..7f1fab4b1c 100644
--- a/src/Avalonia.Native/WindowImplBase.cs
+++ b/src/Avalonia.Native/WindowImplBase.cs
@@ -24,7 +24,7 @@ namespace Avalonia.Native
private object _syncRoot = new object();
private bool _deferredRendering = false;
private bool _gpu = false;
- private readonly IMouseDevice _mouse;
+ private readonly MouseDevice _mouse;
private readonly IKeyboardDevice _keyboard;
private readonly IStandardCursorFactory _cursorFactory;
private Size _savedLogicalSize;
@@ -38,7 +38,7 @@ namespace Avalonia.Native
_deferredRendering = opts.UseDeferredRendering;
_keyboard = AvaloniaLocator.Current.GetService();
- _mouse = AvaloniaLocator.Current.GetService();
+ _mouse = new MouseDevice();
_cursorFactory = AvaloniaLocator.Current.GetService();
}
@@ -142,6 +142,7 @@ namespace Avalonia.Native
{
n?.Dispose();
}
+ _parent._mouse.Dispose();
}
void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke();
diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs
index a486723d86..b57b4a0ca8 100644
--- a/src/Avalonia.Visuals/Media/FontFamily.cs
+++ b/src/Avalonia.Visuals/Media/FontFamily.cs
@@ -5,12 +5,16 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.Fonts;
-using Avalonia.Platform;
namespace Avalonia.Media
{
- public class FontFamily
+ public sealed class FontFamily
{
+ static FontFamily()
+ {
+ Default = new FontFamily(FontManager.Default.DefaultFontFamilyName);
+ }
+
///
///
/// Initializes a new instance of the class.
@@ -30,9 +34,7 @@ namespace Avalonia.Media
{
if (string.IsNullOrEmpty(name))
{
- FamilyNames = new FamilyNameCollection(string.Empty);
-
- return;
+ throw new ArgumentNullException(nameof(name));
}
var fontFamilySegment = GetFontFamilyIdentifier(name);
@@ -53,13 +55,16 @@ namespace Avalonia.Media
///
/// Represents the default font family
///
- public static FontFamily Default => new FontFamily(string.Empty);
+ 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 =>
- AvaloniaLocator.Current.GetService().InstalledFontNames.Select(name => new FontFamily(name));
+ FontManager.Default.GetInstalledFontFamilyNames().Select(name => new FontFamily(name));
///
/// Gets the primary family name of the font family.
@@ -181,7 +186,14 @@ namespace Avalonia.Media
{
var hash = (int)2186146271;
- hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
+ if (Key != null)
+ {
+ hash = (hash * 15768619) ^ Key.GetHashCode();
+ }
+ else
+ {
+ hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
+ }
if (Key != null)
{
diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs
new file mode 100644
index 0000000000..be1bd269ed
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/FontManager.cs
@@ -0,0 +1,112 @@
+// 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 System.Globalization;
+using Avalonia.Platform;
+
+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 static readonly FontManager Default = CreateDefault();
+
+ ///
+ /// Gets the system's default font family's name.
+ ///
+ public string DefaultFontFamilyName
+ {
+ get;
+ protected set;
+ }
+
+ ///
+ /// Get all installed fonts in the system.
+ /// If true the font collection is updated.
+ ///
+ public abstract IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false);
+
+ ///
+ /// Get a cached typeface from specified parameters.
+ ///
+ /// The font family.
+ /// The font weight.
+ /// The font style.
+ ///
+ /// The cached typeface.
+ ///
+ public abstract Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle);
+
+ ///
+ /// Tries to match a specified character to a typeface that supports specified font properties.
+ /// Returns null if no fallback was found.
+ ///
+ /// The codepoint to match against.
+ /// The font weight.
+ /// The font style.
+ /// The font family. This is optional and used for fallback lookup.
+ /// The culture.
+ ///
+ /// The matched typeface.
+ ///
+ public abstract Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+ FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null);
+
+ public static FontManager CreateDefault()
+ {
+ var platformImpl = AvaloniaLocator.Current.GetService();
+
+ if (platformImpl != null)
+ {
+ return new PlatformFontManager(platformImpl);
+ }
+
+ return new EmptyFontManager();
+ }
+
+ private class PlatformFontManager : FontManager
+ {
+ private readonly IFontManagerImpl _platformImpl;
+
+ public PlatformFontManager(IFontManagerImpl platformImpl)
+ {
+ _platformImpl = platformImpl;
+
+ DefaultFontFamilyName = _platformImpl.DefaultFontFamilyName;
+ }
+
+ public override IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
+ _platformImpl.GetInstalledFontFamilyNames(checkForUpdates);
+
+ public override Typeface GetCachedTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle) =>
+ _platformImpl.GetTypeface(fontFamily, fontWeight, fontStyle);
+
+ public override Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default,
+ FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null) =>
+ _platformImpl.MatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture);
+ }
+
+ private class EmptyFontManager : FontManager
+ {
+ public 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;
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
index acf0bbdb11..e777d93315 100644
--- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
+++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
@@ -9,7 +9,7 @@ using System.Text;
namespace Avalonia.Media.Fonts
{
- public class FamilyNameCollection : IEnumerable
+ public sealed class FamilyNameCollection : IReadOnlyList
{
///
/// Initializes a new instance of the class.
@@ -130,5 +130,9 @@ namespace Avalonia.Media.Fonts
return other.ToString().Equals(ToString());
}
+
+ public int Count => Names.Count;
+
+ public string this[int index] => Names[index];
}
}
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index e20e03e296..5013f925b3 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -16,9 +16,10 @@ namespace Avalonia.Media
private IFormattedTextImpl _platformImpl;
private IReadOnlyList _spans;
private Typeface _typeface;
+ private double _fontSize;
private string _text;
private TextAlignment _textAlignment;
- private TextWrapping _wrapping;
+ private TextWrapping _textWrapping;
///
/// Initializes a new instance of the class.
@@ -37,6 +38,31 @@ namespace Avalonia.Media
_platform = platform;
}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public FormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
+ TextWrapping textWrapping, Size constraint)
+ {
+ _text = text;
+
+ _typeface = typeface;
+
+ _fontSize = fontSize;
+
+ _textAlignment = textAlignment;
+
+ _textWrapping = textWrapping;
+
+ _constraint = constraint;
+ }
+
///
/// Gets the bounds of the text within the .
///
@@ -61,6 +87,16 @@ namespace Avalonia.Media
set => Set(ref _typeface, value);
}
+
+ ///
+ /// Gets or sets the font size.
+ ///
+ public double FontSize
+ {
+ get => _fontSize;
+ set => Set(ref _fontSize, value);
+ }
+
///
/// Gets or sets a collection of spans that describe the formatting of subsections of the
/// text.
@@ -92,10 +128,10 @@ namespace Avalonia.Media
///
/// Gets or sets the text wrapping.
///
- public TextWrapping Wrapping
+ public TextWrapping TextWrapping
{
- get => _wrapping;
- set => Set(ref _wrapping, value);
+ get => _textWrapping;
+ set => Set(ref _textWrapping, value);
}
///
@@ -110,8 +146,9 @@ namespace Avalonia.Media
_platformImpl = _platform.CreateFormattedText(
_text,
_typeface,
+ _fontSize,
_textAlignment,
- _wrapping,
+ _textWrapping,
_constraint,
_spans);
}
diff --git a/src/Avalonia.Visuals/Media/GlyphTypeface.cs b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
new file mode 100644
index 0000000000..3ba31f7e84
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/GlyphTypeface.cs
@@ -0,0 +1,111 @@
+// 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;
+
+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(IGlyphTypefaceImpl platformImpl)
+ {
+ PlatformImpl = platformImpl;
+ }
+
+ public IGlyphTypefaceImpl PlatformImpl { get; }
+
+ ///
+ /// Gets the font design units per em.
+ ///
+ public short DesignEmHeight => PlatformImpl.DesignEmHeight;
+
+ ///
+ /// Gets the recommended distance above the baseline in design em size.
+ ///
+ public int Ascent => PlatformImpl.Ascent;
+
+ ///
+ /// Gets the recommended distance under the baseline in design em size.
+ ///
+ public int Descent => PlatformImpl.Descent;
+
+ ///
+ /// Gets the recommended additional space between two lines of text in design em size.
+ ///
+ public int LineGap => PlatformImpl.LineGap;
+
+ ///
+ /// Gets the recommended line height.
+ ///
+ public int LineHeight => Descent - Ascent + LineGap;
+
+ ///
+ /// Gets a value that indicates the distance of the underline from the baseline in design em size.
+ ///
+ public int UnderlinePosition => PlatformImpl.UnderlinePosition;
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ public int UnderlineThickness => PlatformImpl.UnderlineThickness;
+
+ ///
+ /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
+ ///
+ public int StrikethroughPosition => PlatformImpl.StrikethroughPosition;
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ public int StrikethroughThickness => PlatformImpl.StrikethroughThickness;
+
+ ///
+ /// Returns an glyph index for the specified codepoint.
+ ///
+ ///
+ /// Returns 0 if a glyph isn't found.
+ ///
+ /// The codepoint.
+ ///
+ /// A glyph index.
+ ///
+ public ushort GetGlyph(uint codepoint) => PlatformImpl.GetGlyph(codepoint);
+
+ ///
+ /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0.
+ ///
+ /// The codepoints to map.
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints) => PlatformImpl.GetGlyphs(codepoints);
+
+ ///
+ /// Returns the glyph advance for the specified glyph.
+ ///
+ /// The glyph.
+ ///
+ /// The advance.
+ ///
+ public int GetGlyphAdvance(ushort glyph) => PlatformImpl.GetGlyphAdvance(glyph);
+
+ ///
+ /// Returns an array of glyph advances in design em size.
+ ///
+ /// The glyph indices.
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs) => PlatformImpl.GetGlyphAdvances(glyphs);
+
+ void IDisposable.Dispose()
+ {
+ PlatformImpl?.Dispose();
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs
index 37ac0953bf..a6d5c8a43c 100644
--- a/src/Avalonia.Visuals/Media/Typeface.cs
+++ b/src/Avalonia.Visuals/Media/Typeface.cs
@@ -1,39 +1,38 @@
-using System;
+// 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.Diagnostics;
+using JetBrains.Annotations;
namespace Avalonia.Media
{
///
/// Represents a typeface.
///
- public class Typeface
+ [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
+ public class Typeface : IEquatable
{
public static readonly Typeface Default = new Typeface(FontFamily.Default);
+ private GlyphTypeface _glyphTypeface;
+
///
/// Initializes a new instance of the class.
///
/// The font family.
- /// The font size, in DIPs.
- /// The font style.
/// The font weight.
- public Typeface(
- FontFamily fontFamily,
- double fontSize = 12,
- FontStyle style = FontStyle.Normal,
- FontWeight weight = FontWeight.Normal)
+ /// The font style.
+ public Typeface([NotNull]FontFamily fontFamily,
+ FontWeight weight = FontWeight.Normal,
+ FontStyle style = FontStyle.Normal)
{
- if (fontSize <= 0)
- {
- throw new ArgumentException("Font size must be > 0.");
- }
-
if (weight <= 0)
{
throw new ArgumentException("Font weight must be > 0.");
}
FontFamily = fontFamily;
- FontSize = fontSize;
Style = style;
Weight = weight;
}
@@ -42,15 +41,12 @@ namespace Avalonia.Media
/// Initializes a new instance of the class.
///
/// The name of the font family.
- /// The font size, in DIPs.
/// The font style.
/// The font weight.
- public Typeface(
- string fontFamilyName,
- double fontSize = 12,
- FontStyle style = FontStyle.Normal,
- FontWeight weight = FontWeight.Normal)
- : this(new FontFamily(fontFamilyName), fontSize, style, weight)
+ public Typeface(string fontFamilyName,
+ FontWeight weight = FontWeight.Normal,
+ FontStyle style = FontStyle.Normal)
+ : this(new FontFamily(fontFamilyName), weight, style)
{
}
@@ -59,11 +55,6 @@ namespace Avalonia.Media
///
public FontFamily FontFamily { get; }
- ///
- /// Gets the size of the font in DIPs.
- ///
- public double FontSize { get; }
-
///
/// Gets the font style.
///
@@ -73,5 +64,59 @@ namespace Avalonia.Media
/// Gets the font weight.
///
public FontWeight Weight { get; }
+
+ ///
+ /// Gets the glyph typeface.
+ ///
+ ///
+ /// The glyph typeface.
+ ///
+ public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this));
+
+ public static bool operator !=(Typeface a, Typeface b)
+ {
+ return !(a == b);
+ }
+
+ public static bool operator ==(Typeface a, Typeface b)
+ {
+ if (ReferenceEquals(a, b))
+ {
+ return true;
+ }
+
+ return !(a is null) && a.Equals(b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is Typeface typeface)
+ {
+ return Equals(typeface);
+ }
+
+ return false;
+ }
+
+ public bool Equals(Typeface other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight;
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = (FontFamily != null ? FontFamily.GetHashCode() : 0);
+ hashCode = (hashCode * 397) ^ (int)Style;
+ hashCode = (hashCode * 397) ^ (int)Weight;
+ return hashCode;
+ }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
new file mode 100644
index 0000000000..254b5d07d1
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
@@ -0,0 +1,48 @@
+// 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 System.Globalization;
+using Avalonia.Media;
+
+namespace Avalonia.Platform
+{
+ public interface IFontManagerImpl
+ {
+ ///
+ /// Gets the system's default font family's name.
+ ///
+ string DefaultFontFamilyName { get; }
+
+ ///
+ /// Get all installed fonts in the system.
+ /// If true the font collection is updated.
+ ///
+ 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.
+ ///
+ /// The codepoint to match against.
+ /// The font weight.
+ /// The font style.
+ /// The font family. This is optional and used for fallback lookup.
+ /// The culture.
+ ///
+ /// The typeface.
+ ///
+ Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null);
+ }
+}
diff --git a/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..8c043a5129
--- /dev/null
+++ b/src/Avalonia.Visuals/Platform/IGlyphTypefaceImpl.cs
@@ -0,0 +1,89 @@
+// 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
+{
+ public interface IGlyphTypefaceImpl : IDisposable
+ {
+ ///
+ /// Gets the font design units per em.
+ ///
+ short DesignEmHeight { get; }
+
+ ///
+ /// Gets the recommended distance above the baseline in design em size.
+ ///
+ int Ascent { get; }
+
+ ///
+ /// Gets the recommended distance under the baseline in design em size.
+ ///
+ int Descent { get; }
+
+ ///
+ /// Gets the recommended additional space between two lines of text in design em size.
+ ///
+ int LineGap { get; }
+
+ ///
+ /// Gets a value that indicates the distance of the underline from the baseline in design em size.
+ ///
+ int UnderlinePosition { get; }
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ int UnderlineThickness { get; }
+
+ ///
+ /// Gets a value that indicates the distance of the strikethrough from the baseline in design em size.
+ ///
+ int StrikethroughPosition { get; }
+
+ ///
+ /// Gets a value that indicates the thickness of the underline in design em size.
+ ///
+ int StrikethroughThickness { get; }
+
+ ///
+ /// Returns an glyph index for the specified codepoint.
+ ///
+ ///
+ /// Returns 0 if a glyph isn't found.
+ ///
+ /// The codepoint.
+ ///
+ /// A glyph index.
+ ///
+ ushort GetGlyph(uint codepoint);
+
+ ///
+ /// Returns an array of glyph indices. Codepoints that are not represented by the font are returned as 0.
+ ///
+ /// The codepoints to map.
+ ///
+ /// An array of glyph indices.
+ ///
+ ushort[] GetGlyphs(ReadOnlySpan codepoints);
+
+ ///
+ /// Returns the glyph advance for the specified glyph.
+ ///
+ /// The glyph.
+ ///
+ /// The advance.
+ ///
+ int GetGlyphAdvance(ushort glyph);
+
+ ///
+ /// Returns an array of glyph advances in design em size.
+ ///
+ /// The glyph indices.
+ ///
+ /// An array of glyph advances.
+ ///
+ int[] GetGlyphAdvances(ReadOnlySpan glyphs);
+ }
+}
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index 87db9251e1..5a0a7b2f19 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -13,16 +13,12 @@ namespace Avalonia.Platform
///
public interface IPlatformRenderInterface
{
- ///
- /// Get all installed fonts in the system
- ///
- IEnumerable InstalledFontNames { get; }
-
///
/// Creates a formatted text implementation.
///
/// The text.
/// The base typeface.
+ /// The font size.
/// The text alignment.
/// The text wrapping mode.
/// The text layout constraints.
@@ -31,6 +27,7 @@ namespace Avalonia.Platform
IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -114,5 +111,14 @@ namespace Avalonia.Platform
/// The number of bytes per row.
/// An .
IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride);
+
+ ///
+ /// Creates a glyph typeface for specified typeface.
+ ///
+ /// The typeface.
+ ///
+ /// The glyph typeface implementation.
+ ///
+ IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface);
}
}
diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs
index 7b10fc1212..e341f02901 100644
--- a/src/Avalonia.Visuals/Rendering/RendererBase.cs
+++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs
@@ -7,7 +7,8 @@ namespace Avalonia.Rendering
{
public class RendererBase
{
- private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18);
+ private static readonly Typeface s_fpsTypeface = new Typeface("Arial");
+ private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond;
private int _fps;
@@ -18,7 +19,8 @@ namespace Avalonia.Rendering
{
_fpsText = new FormattedText
{
- Typeface = s_fpsTypeface
+ Typeface = s_fpsTypeface,
+ FontSize = s_fontSize
};
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
index 558e96e132..1689f2e964 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
@@ -51,7 +51,7 @@ namespace Avalonia.Rendering.SceneGraph
UpdateSize(scene);
}
- if (visual.VisualRoot != null)
+ if (visual.VisualRoot == scene.Root.Visual)
{
if (node?.Parent != null &&
visual.VisualParent != null &&
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index d7a7bb97fd..6ba562bb69 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -19,9 +19,7 @@ namespace Avalonia.X11
class AvaloniaX11Platform : IWindowingPlatform
{
private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice());
- private Lazy _mouseDevice = new Lazy(() => new MouseDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
- public MouseDevice MouseDevice => _mouseDevice.Value;
public Dictionary> Windows = new Dictionary>();
public XI2Manager XI2;
public X11Info Info { get; private set; }
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 17471fad10..32460fed86 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -32,7 +32,8 @@ namespace Avalonia.X11
private PixelPoint? _configurePoint;
private bool _triggeredExpose;
private IInputRoot _inputRoot;
- private readonly IMouseDevice _mouse;
+ private readonly MouseDevice _mouse;
+ private readonly TouchDevice _touch;
private readonly IKeyboardDevice _keyboard;
private PixelPoint? _position;
private PixelSize _realSize;
@@ -57,7 +58,8 @@ namespace Avalonia.X11
_platform = platform;
_popup = popupParent != null;
_x11 = platform.Info;
- _mouse = platform.MouseDevice;
+ _mouse = new MouseDevice();
+ _touch = new TouchDevice();
_keyboard = platform.KeyboardDevice;
var glfeature = AvaloniaLocator.Current.GetService();
@@ -702,6 +704,8 @@ namespace Avalonia.X11
_platform.XI2?.OnWindowDestroyed(_handle);
_handle = IntPtr.Zero;
Closed?.Invoke();
+ _mouse.Dispose();
+ _touch.Dispose();
}
if (_useRenderWindow && _renderHandle != IntPtr.Zero)
@@ -830,6 +834,8 @@ namespace Avalonia.X11
}
public IMouseDevice MouseDevice => _mouse;
+ public TouchDevice TouchDevice => _touch;
+
public IPopupImpl CreatePopup()
=> _platform.Options.OverlayPopups ? null : new X11Window(_platform, this);
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index 6989d6d26d..e37ed39bee 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/src/Avalonia.X11/XI2Manager.cs
@@ -92,8 +92,6 @@ namespace Avalonia.X11
private PointerDeviceInfo _pointerDevice;
private AvaloniaX11Platform _platform;
- private readonly TouchDevice _touchDevice = new TouchDevice();
-
public bool Init(AvaloniaX11Platform platform)
{
@@ -198,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd);
- client.ScheduleInput(new RawTouchEventArgs(_touchDevice,
+ client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return;
}
@@ -232,10 +230,10 @@ namespace Avalonia.X11
}
if (scrollDelta != default)
- client.ScheduleInput(new RawMouseWheelEventArgs(_platform.MouseDevice, ev.Timestamp,
+ client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev))
- client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+ client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
@@ -248,7 +246,7 @@ namespace Avalonia.X11
: ev.Button == 3 ? (down ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp)
: (RawPointerEventType?)null;
if (type.HasValue)
- client.ScheduleInput(new RawPointerEventArgs(_platform.MouseDevice, ev.Timestamp, client.InputRoot,
+ client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers));
}
@@ -310,5 +308,7 @@ namespace Avalonia.X11
{
IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args);
+ IMouseDevice MouseDevice { get; }
+ TouchDevice TouchDevice { get; }
}
}
diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
index 4f884cdf33..68da513528 100644
--- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
+++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
@@ -12,5 +12,6 @@
+
diff --git a/src/Skia/Avalonia.Skia/FontKey.cs b/src/Skia/Avalonia.Skia/FontKey.cs
new file mode 100644
index 0000000000..bb3fe230c1
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/FontKey.cs
@@ -0,0 +1,40 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Skia
+{
+ internal readonly struct FontKey : IEquatable
+ {
+ public readonly FontStyle Style;
+ public readonly FontWeight Weight;
+
+ public FontKey(FontWeight weight, FontStyle style)
+ {
+ Style = style;
+ Weight = weight;
+ }
+
+ public override int GetHashCode()
+ {
+ var hash = 17;
+ hash = hash * 31 + (int)Style;
+ hash = hash * 31 + (int)Weight;
+
+ return hash;
+ }
+
+ public override bool Equals(object other)
+ {
+ return other is FontKey key && Equals(key);
+ }
+
+ public bool Equals(FontKey other)
+ {
+ return Style == other.Style &&
+ Weight == other.Weight;
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
new file mode 100644
index 0000000000..03de82178a
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -0,0 +1,82 @@
+// 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 System.Globalization;
+using Avalonia.Media;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ internal class FontManagerImpl : IFontManagerImpl
+ {
+ private SKFontManager _skFontManager = SKFontManager.Default;
+
+ public FontManagerImpl()
+ {
+ DefaultFontFamilyName = SKTypeface.Default.FamilyName;
+ }
+
+ public string DefaultFontFamilyName { get; }
+
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ {
+ if (checkForUpdates)
+ {
+ _skFontManager = SKFontManager.CreateDefault();
+ }
+
+ return _skFontManager.FontFamilies;
+ }
+
+ public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
+ {
+ return TypefaceCache.Get(fontFamily.Name, fontWeight, fontStyle).Typeface;
+ }
+
+ public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = default, FontStyle fontStyle = default,
+ FontFamily fontFamily = null, CultureInfo culture = null)
+ {
+ var fontFamilyName = FontFamily.Default.Name;
+
+ if (culture == null)
+ {
+ culture = CultureInfo.CurrentUICulture;
+ }
+
+ 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);
+
+ if (skTypeface == null)
+ {
+ continue;
+ }
+
+ fontFamilyName = familyName;
+
+ break;
+ }
+ }
+ else
+ {
+ var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal,
+ (SKFontStyleSlant)fontStyle,
+ new[] { culture.TwoLetterISOLanguageName, culture.ThreeLetterISOLanguageName }, codepoint);
+
+ if (skTypeface != null)
+ {
+ fontFamilyName = skTypeface.FamilyName;
+ }
+ }
+
+ return GetTypeface(fontFamilyName, fontWeight, fontStyle);
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
index eb7b65cdce..a9358cb458 100644
--- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
+++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs
@@ -18,6 +18,7 @@ namespace Avalonia.Skia
public FormattedTextImpl(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -28,47 +29,22 @@ namespace Avalonia.Skia
// Replace 0 characters with zero-width spaces (200B)
Text = Text.Replace((char)0, (char)0x200B);
- SKTypeface skiaTypeface = null;
+ var entry = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style);
- if (typeface.FontFamily.Key != null)
+ _paint = new SKPaint
{
- var typefaces = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
- skiaTypeface = typefaces.GetTypeFace(typeface);
- }
- else
- {
- if (typeface.FontFamily.FamilyNames.HasFallbacks)
- {
- foreach (var familyName in typeface.FontFamily.FamilyNames)
- {
- skiaTypeface = TypefaceCache.GetTypeface(
- familyName,
- typeface.Style,
- typeface.Weight);
- if (skiaTypeface.FamilyName != TypefaceCache.DefaultFamilyName) break;
- }
- }
- else
- {
- skiaTypeface = TypefaceCache.GetTypeface(
- typeface.FontFamily.Name,
- typeface.Style,
- typeface.Weight);
- }
- }
-
- _paint = new SKPaint();
+ TextEncoding = SKTextEncoding.Utf16,
+ IsStroke = false,
+ IsAntialias = true,
+ LcdRenderText = true,
+ SubpixelText = true,
+ Typeface = entry.SKTypeface,
+ TextSize = (float)fontSize,
+ TextAlign = textAlignment.ToSKTextAlign()
+ };
//currently Skia does not measure properly with Utf8 !!!
//Paint.TextEncoding = SKTextEncoding.Utf8;
- _paint.TextEncoding = SKTextEncoding.Utf16;
- _paint.IsStroke = false;
- _paint.IsAntialias = true;
- _paint.LcdRenderText = true;
- _paint.SubpixelText = true;
- _paint.Typeface = skiaTypeface;
- _paint.TextSize = (float)typeface.FontSize;
- _paint.TextAlign = textAlignment.ToSKTextAlign();
_wrapping = wrapping;
_constraint = constraint;
@@ -99,7 +75,24 @@ namespace Avalonia.Skia
public TextHitTestResult HitTestPoint(Point point)
{
float y = (float)point.Y;
- var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y);
+
+ AvaloniaFormattedTextLine line = default;
+
+ float nextTop = 0;
+
+ foreach(var currentLine in _skiaLines)
+ {
+ if(currentLine.Top <= y)
+ {
+ line = currentLine;
+ nextTop = currentLine.Top + currentLine.Height;
+ }
+ else
+ {
+ nextTop = currentLine.Top;
+ break;
+ }
+ }
if (!line.Equals(default(AvaloniaFormattedTextLine)))
{
@@ -127,12 +120,15 @@ namespace Avalonia.Skia
line.Length : (line.Length - 1);
}
- return new TextHitTestResult
+ if (y < nextTop)
{
- IsInside = false,
- TextPosition = line.Start + offset,
- IsTrailing = Text.Length == (line.Start + offset + 1)
- };
+ return new TextHitTestResult
+ {
+ IsInside = false,
+ TextPosition = line.Start + offset,
+ IsTrailing = Text.Length == (line.Start + offset + 1)
+ };
+ }
}
bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height);
diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..e46f766255
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
@@ -0,0 +1,179 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Media;
+using Avalonia.Platform;
+using HarfBuzzSharp;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+ public class GlyphTypefaceImpl : IGlyphTypefaceImpl
+ {
+ private bool _isDisposed;
+
+ public GlyphTypefaceImpl(Typeface typeface)
+ {
+ Typeface = TypefaceCache.Get(typeface.FontFamily, typeface.Weight, typeface.Style).SKTypeface;
+
+ Face = new Face(GetTable)
+ {
+ UnitsPerEm = Typeface.UnitsPerEm
+ };
+
+ Font = new Font(Face);
+
+ Font.SetFunctionsOpenType();
+
+ Font.GetScale(out var xScale, out _);
+
+ DesignEmHeight = (short)xScale;
+
+ if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
+ {
+ Font.TryGetVerticalFontExtents(out fontExtents);
+ }
+
+ Ascent = -fontExtents.Ascender;
+
+ Descent = -fontExtents.Descender;
+
+ LineGap = fontExtents.LineGap;
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
+ {
+ UnderlinePosition = underlinePosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
+ {
+ UnderlineThickness = underlineThickness;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
+ {
+ StrikethroughPosition = strikethroughPosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
+ {
+ StrikethroughThickness = strikethroughThickness;
+ }
+ }
+
+ public Face Face { get; }
+
+ public Font Font { get; }
+
+ public SKTypeface Typeface { get; }
+
+ ///
+ public short DesignEmHeight { get; }
+
+ ///
+ public int Ascent { get; }
+
+ ///
+ public int Descent { get; }
+
+ ///
+ public int LineGap { get; }
+
+ //ToDo: Get these values from HarfBuzz
+ ///
+ public int UnderlinePosition { get; }
+
+ ///
+ public int UnderlineThickness { get; }
+
+ ///
+ public int StrikethroughPosition { get; }
+
+ ///
+ public int StrikethroughThickness { get; }
+
+ ///
+ public ushort GetGlyph(uint codepoint)
+ {
+ if (Font.TryGetGlyph(codepoint, out var glyph))
+ {
+ return (ushort)glyph;
+ }
+
+ return 0;
+ }
+
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ var glyphs = new ushort[codepoints.Length];
+
+ for (var i = 0; i < codepoints.Length; i++)
+ {
+ if (Font.TryGetGlyph(codepoints[i], out var glyph))
+ {
+ glyphs[i] = (ushort)glyph;
+ }
+ }
+
+ return glyphs;
+ }
+
+ ///
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return Font.GetHorizontalGlyphAdvance(glyph);
+ }
+
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var glyphIndices = new uint[glyphs.Length];
+
+ for (var i = 0; i < glyphs.Length; i++)
+ {
+ glyphIndices[i] = glyphs[i];
+ }
+
+ return Font.GetHorizontalGlyphAdvances(glyphIndices);
+ }
+
+ private Blob GetTable(Face face, Tag tag)
+ {
+ var size = Typeface.GetTableSize(tag);
+
+ var data = Marshal.AllocCoTaskMem(size);
+
+ var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data));
+
+ return Typeface.TryGetTableData(tag, 0, size, data) ?
+ new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null;
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+
+ if (!disposing)
+ {
+ return;
+ }
+
+ Font?.Dispose();
+ Face?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 15f38b1c4f..ee0cfb2f06 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls.Platform.Surfaces;
@@ -17,12 +18,13 @@ namespace Avalonia.Skia
///
internal class PlatformRenderInterface : IPlatformRenderInterface
{
+ private readonly ConcurrentDictionary _glyphTypefaceCache =
+ new ConcurrentDictionary();
+
private readonly ICustomSkiaGpu _customSkiaGpu;
private GRContext GrContext { get; }
- public IEnumerable InstalledFontNames => SKFontManager.Default.FontFamilies;
-
public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
{
if (customSkiaGpu != null)
@@ -52,12 +54,13 @@ namespace Avalonia.Skia
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList spans)
{
- return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
+ return new FormattedTextImpl(text, typeface,fontSize, textAlignment, wrapping, constraint, spans);
}
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
@@ -151,5 +154,10 @@ namespace Avalonia.Skia
{
return new WriteableBitmapImpl(size, dpi, format);
}
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+ }
}
}
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
index 17448127b0..8ec2a9c3f8 100644
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
@@ -4,114 +4,59 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
-
using Avalonia.Media;
-using SkiaSharp;
-
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
- private readonly ConcurrentDictionary> _fontFamilies =
- new ConcurrentDictionary>();
+ private readonly ConcurrentDictionary> _fontFamilies =
+ new ConcurrentDictionary>();
- public void AddTypeFace(SKTypeface typeface)
+ public void AddEntry(string familyName, FontKey key, TypefaceCollectionEntry entry)
{
- var key = new FontKey((SKFontStyleWeight)typeface.FontWeight, typeface.FontSlant);
-
- if (!_fontFamilies.TryGetValue(typeface.FamilyName, out var fontFamily))
+ if (!_fontFamilies.TryGetValue(familyName, out var fontFamily))
{
- fontFamily = new ConcurrentDictionary();
+ fontFamily = new ConcurrentDictionary();
- _fontFamilies.TryAdd(typeface.FamilyName, fontFamily);
+ _fontFamilies.TryAdd(familyName, fontFamily);
}
- fontFamily.TryAdd(key, typeface);
+ fontFamily.TryAdd(key, entry);
}
- public SKTypeface GetTypeFace(Typeface typeface)
+ public TypefaceCollectionEntry Get(string familyName, FontWeight fontWeight, FontStyle fontStyle)
{
- var styleSlant = SKFontStyleSlant.Upright;
-
- switch (typeface.Style)
- {
- case FontStyle.Italic:
- styleSlant = SKFontStyleSlant.Italic;
- break;
-
- case FontStyle.Oblique:
- styleSlant = SKFontStyleSlant.Oblique;
- break;
- }
+ var key = new FontKey(fontWeight, fontStyle);
- if (!_fontFamilies.TryGetValue(typeface.FontFamily.Name, out var fontFamily))
- {
- return TypefaceCache.GetTypeface(TypefaceCache.DefaultFamilyName, typeface.Style, typeface.Weight);
- }
-
- var weight = (SKFontStyleWeight)typeface.Weight;
-
- var key = new FontKey(weight, styleSlant);
-
- return fontFamily.GetOrAdd(key, GetFallback(fontFamily, key));
+ return _fontFamilies.TryGetValue(familyName, out var fontFamily) ?
+ fontFamily.GetOrAdd(key, GetFallback(fontFamily, key)) :
+ null;
}
- private static SKTypeface GetFallback(IDictionary fontFamily, FontKey key)
+ private static TypefaceCollectionEntry GetFallback(IDictionary fontFamily, FontKey key)
{
var keys = fontFamily.Keys.Where(
- x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Slant == key.Slant).ToArray();
+ 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(
- x => x.Weight == key.Weight && (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray();
+ x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray();
if (!keys.Any())
{
keys = fontFamily.Keys.Where(
x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) &&
- (x.Slant >= key.Slant || x.Slant < key.Slant)).ToArray();
+ (x.Style >= key.Style || x.Style < key.Style)).ToArray();
}
}
key = keys.FirstOrDefault();
- fontFamily.TryGetValue(key, out var typeface);
-
- return typeface;
- }
-
- private struct FontKey
- {
- public readonly SKFontStyleSlant Slant;
- public readonly SKFontStyleWeight Weight;
-
- public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
- {
- Slant = slant;
- Weight = weight;
- }
-
- public override int GetHashCode()
- {
- var hash = 17;
- hash = (hash * 31) + (int)Slant;
- hash = (hash * 31) + (int)Weight;
-
- return hash;
- }
+ fontFamily.TryGetValue(key, out var entry);
- public override bool Equals(object other)
- {
- return other is FontKey key && this.Equals(key);
- }
-
- private bool Equals(FontKey other)
- {
- return Slant == other.Slant &&
- Weight == other.Weight;
- }
+ return entry;
}
}
}
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
index ab8ee85a54..4bb42c7118 100644
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
@@ -45,9 +45,13 @@ namespace Avalonia.Skia
{
var assetStream = assetLoader.Open(asset);
- var typeface = SKTypeface.FromStream(assetStream);
+ var skTypeface = SKTypeface.FromStream(assetStream);
- typeFaceCollection.AddTypeFace(typeface);
+ var typeface = new Typeface(fontFamily, (FontWeight)skTypeface.FontWeight, (FontStyle)skTypeface.FontSlant);
+
+ var entry = new TypefaceCollectionEntry(typeface, skTypeface);
+
+ typeFaceCollection.AddEntry(skTypeface.FamilyName, new FontKey(typeface.Weight, typeface.Style), entry);
}
return typeFaceCollection;
diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs
index f16e967f42..ce3aef755b 100644
--- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs
+++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs
@@ -25,6 +25,11 @@ 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
index 9e270114d2..1c2b855032 100644
--- a/src/Skia/Avalonia.Skia/TypefaceCache.cs
+++ b/src/Skia/Avalonia.Skia/TypefaceCache.cs
@@ -1,7 +1,7 @@
// 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 System.Collections.Concurrent;
using Avalonia.Media;
using SkiaSharp;
@@ -12,88 +12,36 @@ namespace Avalonia.Skia
///
internal static class TypefaceCache
{
- public static readonly string DefaultFamilyName = CreateDefaultFamilyName();
+ private static readonly ConcurrentDictionary> s_cache =
+ new ConcurrentDictionary>();
- private static readonly Dictionary> s_cache =
- new Dictionary>();
-
- struct FontKey
+ public static TypefaceCollectionEntry Get(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
{
- public readonly SKFontStyleSlant Slant;
- public readonly SKFontStyleWeight Weight;
-
- public FontKey(SKFontStyleWeight weight, SKFontStyleSlant slant)
+ if (fontFamily.Key != null)
{
- Slant = slant;
- Weight = weight;
+ return SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily)
+ .Get(fontFamily.Name, fontWeight, fontStyle);
}
- public override int GetHashCode()
- {
- int hash = 17;
- hash = hash * 31 + (int)Slant;
- hash = hash * 31 + (int)Weight;
-
- return hash;
- }
-
- public override bool Equals(object other)
- {
- return other is FontKey ? Equals((FontKey)other) : false;
- }
-
- public bool Equals(FontKey other)
- {
- return Slant == other.Slant &&
- Weight == other.Weight;
- }
-
- // Equals and GetHashCode ommitted
- }
-
- private static string CreateDefaultFamilyName()
- {
- var defaultTypeface = SKTypeface.CreateDefault();
+ var typefaceCollection = s_cache.GetOrAdd(fontFamily.Name, new ConcurrentDictionary());
- return defaultTypeface.FamilyName;
- }
+ var key = new FontKey(fontWeight, fontStyle);
- private static SKTypeface GetTypeface(string name, FontKey key)
- {
- var familyKey = name;
-
- if (!s_cache.TryGetValue(familyKey, out var entry))
+ if (typefaceCollection.TryGetValue(key, out var entry))
{
- s_cache[familyKey] = entry = new Dictionary();
+ return entry;
}
- if (!entry.TryGetValue(key, out var typeface))
- {
- typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant) ??
- GetTypeface(DefaultFamilyName, key);
+ var skTypeface = SKTypeface.FromFamilyName(fontFamily.Name, (SKFontStyleWeight)fontWeight,
+ SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle) ?? SKTypeface.Default;
- entry[key] = typeface;
- }
+ var typeface = new Typeface(fontFamily.Name, fontWeight, fontStyle);
- return typeface;
- }
-
- public static SKTypeface GetTypeface(string name, FontStyle style, FontWeight weight)
- {
- var skStyle = SKFontStyleSlant.Upright;
+ entry = new TypefaceCollectionEntry(typeface, skTypeface);
- switch (style)
- {
- case FontStyle.Italic:
- skStyle = SKFontStyleSlant.Italic;
- break;
-
- case FontStyle.Oblique:
- skStyle = SKFontStyleSlant.Oblique;
- break;
- }
+ typefaceCollection[key] = entry;
- return GetTypeface(name, new FontKey((SKFontStyleWeight)weight, skStyle));
+ return entry;
}
}
}
diff --git a/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
new file mode 100644
index 0000000000..ef9f889819
--- /dev/null
+++ b/src/Skia/Avalonia.Skia/TypefaceCollectionEntry.cs
@@ -0,0 +1,19 @@
+// 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/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
index 458d8f9cbb..7d47b95ede 100644
--- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
+++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
@@ -14,6 +14,7 @@
+
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 5ab9a8f74d..1bda5157a5 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using Avalonia.Controls;
@@ -27,6 +28,8 @@ 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; }
@@ -41,20 +44,6 @@ namespace Avalonia.Direct2D1
public static SharpDX.DXGI.Device1 DxgiDevice { get; private set; }
- public IEnumerable InstalledFontNames
- {
- get
- {
- var cache = Direct2D1FontCollectionCache.s_installedFontCollection;
- var length = cache.FontFamilyCount;
- for (int i = 0; i < length; i++)
- {
- var names = cache.GetFontFamily(i).FamilyNames;
- yield return names.GetString(0);
- }
- }
- }
-
private static readonly object s_initLock = new object();
private static bool s_initialized = false;
@@ -120,6 +109,7 @@ namespace Avalonia.Direct2D1
{
InitializeDirect2D();
AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance);
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(new FontManagerImpl());
SharpDX.Configuration.EnableReleaseOnFinalizer = true;
}
@@ -131,6 +121,7 @@ namespace Avalonia.Direct2D1
public IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -139,6 +130,7 @@ namespace Avalonia.Direct2D1
return new FormattedTextImpl(
text,
typeface,
+ fontSize,
textAlignment,
wrapping,
constraint,
@@ -201,5 +193,10 @@ namespace Avalonia.Direct2D1
{
return new WicBitmapImpl(format, data, size, dpi, stride);
}
+
+ public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+ {
+ return _glyphTypefaceCache.GetOrAdd(typeface, new GlyphTypefaceImpl(typeface));
+ }
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
index d93a59d384..b455c4fbee 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/Direct2D1FontCollectionCache.cs
@@ -1,62 +1,61 @@
using System.Collections.Concurrent;
using Avalonia.Media;
using Avalonia.Media.Fonts;
+using SharpDX.DirectWrite;
+using FontFamily = Avalonia.Media.FontFamily;
+using FontStyle = SharpDX.DirectWrite.FontStyle;
+using FontWeight = SharpDX.DirectWrite.FontWeight;
namespace Avalonia.Direct2D1.Media
{
internal static class Direct2D1FontCollectionCache
{
- private static readonly ConcurrentDictionary s_cachedCollections;
- internal static readonly SharpDX.DirectWrite.FontCollection s_installedFontCollection;
+ private static readonly ConcurrentDictionary s_cachedCollections;
+ internal static readonly FontCollection InstalledFontCollection;
static Direct2D1FontCollectionCache()
{
- s_cachedCollections = new ConcurrentDictionary();
+ s_cachedCollections = new ConcurrentDictionary();
- s_installedFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
+ InstalledFontCollection = Direct2D1Platform.DirectWriteFactory.GetSystemFontCollection(false);
}
- public static SharpDX.DirectWrite.TextFormat GetTextFormat(Typeface typeface)
+ public static Font GetFont(Typeface typeface)
{
var fontFamily = typeface.FontFamily;
var fontCollection = GetOrAddFontCollection(fontFamily);
- var fontFamilyName = FontFamily.Default.Name;
- // Should this be cached?
foreach (var familyName in fontFamily.FamilyNames)
{
- if (!fontCollection.FindFamilyName(familyName, out _))
+ if (fontCollection.FindFamilyName(familyName, out var index))
{
- continue;
+ return fontCollection.GetFontFamily(index).GetFirstMatchingFont(
+ (FontWeight)typeface.Weight,
+ FontStretch.Normal,
+ (FontStyle)typeface.Style);
}
-
- fontFamilyName = familyName;
-
- break;
}
- return new SharpDX.DirectWrite.TextFormat(
- Direct2D1Platform.DirectWriteFactory,
- fontFamilyName,
- fontCollection,
- (SharpDX.DirectWrite.FontWeight)typeface.Weight,
- (SharpDX.DirectWrite.FontStyle)typeface.Style,
- SharpDX.DirectWrite.FontStretch.Normal,
- (float)typeface.FontSize);
+ InstalledFontCollection.FindFamilyName(FontFamily.Default.Name, out var i);
+
+ return InstalledFontCollection.GetFontFamily(i).GetFirstMatchingFont(
+ (FontWeight)typeface.Weight,
+ FontStretch.Normal,
+ (FontStyle)typeface.Style);
}
- private static SharpDX.DirectWrite.FontCollection GetOrAddFontCollection(FontFamily fontFamily)
+ private static FontCollection GetOrAddFontCollection(FontFamily fontFamily)
{
- return fontFamily.Key == null ? s_installedFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
+ return fontFamily.Key == null ? InstalledFontCollection : s_cachedCollections.GetOrAdd(fontFamily.Key, CreateFontCollection);
}
- private static SharpDX.DirectWrite.FontCollection CreateFontCollection(FontFamilyKey key)
+ private static FontCollection CreateFontCollection(FontFamilyKey key)
{
var assets = FontFamilyLoader.LoadFontAssets(key);
var fontLoader = new DWriteResourceFontLoader(Direct2D1Platform.DirectWriteFactory, assets);
- return new SharpDX.DirectWrite.FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
+ return new FontCollection(Direct2D1Platform.DirectWriteFactory, fontLoader, fontLoader.Key);
}
}
}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
new file mode 100644
index 0000000000..94de397652
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
@@ -0,0 +1,71 @@
+// 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 System.Globalization;
+using Avalonia.Media;
+using Avalonia.Platform;
+using SharpDX.DirectWrite;
+using FontFamily = Avalonia.Media.FontFamily;
+using FontStyle = Avalonia.Media.FontStyle;
+using FontWeight = Avalonia.Media.FontWeight;
+
+namespace Avalonia.Direct2D1.Media
+{
+ internal class FontManagerImpl : IFontManagerImpl
+ {
+ public FontManagerImpl()
+ {
+ //ToDo: Implement a real lookup of the system's default font.
+ DefaultFontFamilyName = "segoe ui";
+ }
+
+ public string DefaultFontFamilyName { get; }
+
+ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false)
+ {
+ var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
+
+ var fontFamilies = new string[familyCount];
+
+ for (var i = 0; i < familyCount; i++)
+ {
+ fontFamilies[i] = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i).FamilyNames.GetString(0);
+ }
+
+ return fontFamilies;
+ }
+
+ public Typeface GetTypeface(FontFamily fontFamily, FontWeight fontWeight, FontStyle fontStyle)
+ {
+ //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++)
+ {
+ var font = Direct2D1FontCollectionCache.InstalledFontCollection.GetFontFamily(i)
+ .GetMatchingFonts((SharpDX.DirectWrite.FontWeight)fontWeight, FontStretch.Normal,
+ (SharpDX.DirectWrite.FontStyle)fontStyle).GetFont(0);
+
+ if (!font.HasCharacter(codepoint))
+ {
+ continue;
+ }
+
+ fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
+
+ break;
+ }
+
+ return GetTypeface(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
index b73deb1f0a..b1a177ad24 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
@@ -14,6 +14,7 @@ namespace Avalonia.Direct2D1.Media
public FormattedTextImpl(
string text,
Typeface typeface,
+ double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
@@ -21,20 +22,20 @@ namespace Avalonia.Direct2D1.Media
{
Text = text;
- using (var textFormat = Direct2D1FontCollectionCache.GetTextFormat(typeface))
+ 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))
{
textFormat.WordWrapping =
wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
TextLayout = new DWrite.TextLayout(
- Direct2D1Platform.DirectWriteFactory,
- Text ?? string.Empty,
- textFormat,
- (float)constraint.Width,
- (float)constraint.Height)
- {
- TextAlignment = textAlignment.ToDirect2D()
- };
+ Direct2D1Platform.DirectWriteFactory,
+ Text ?? string.Empty,
+ textFormat,
+ (float)constraint.Width,
+ (float)constraint.Height) { TextAlignment = textAlignment.ToDirect2D() };
}
if (spans != null)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
new file mode 100644
index 0000000000..32def01c39
--- /dev/null
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphTypefaceImpl.cs
@@ -0,0 +1,188 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+using Avalonia.Platform;
+using HarfBuzzSharp;
+using SharpDX.DirectWrite;
+
+namespace Avalonia.Direct2D1.Media
+{
+ public class GlyphTypefaceImpl : IGlyphTypefaceImpl
+ {
+ private bool _isDisposed;
+
+ public GlyphTypefaceImpl(Typeface typeface)
+ {
+ DWFont = Direct2D1FontCollectionCache.GetFont(typeface);
+
+ FontFace = new FontFace(DWFont);
+
+ Face = new Face(GetTable);
+
+ Font = new HarfBuzzSharp.Font(Face);
+
+ Font.SetFunctionsOpenType();
+
+ Font.GetScale(out var xScale, out _);
+
+ DesignEmHeight = (short)xScale;
+
+ if (!Font.TryGetHorizontalFontExtents(out var fontExtents))
+ {
+ Font.TryGetVerticalFontExtents(out fontExtents);
+ }
+
+ Ascent = -fontExtents.Ascender;
+
+ Descent = -fontExtents.Descender;
+
+ LineGap = fontExtents.LineGap;
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition))
+ {
+ UnderlinePosition = underlinePosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness))
+ {
+ UnderlineThickness = underlineThickness;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition))
+ {
+ StrikethroughPosition = strikethroughPosition;
+ }
+
+ if (Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness))
+ {
+ StrikethroughThickness = strikethroughThickness;
+ }
+ }
+
+ private Blob GetTable(Face face, Tag tag)
+ {
+ var dwTag = (int)SwapBytes(tag);
+
+ if (FontFace.TryGetFontTable(dwTag, out var tableData, out _))
+ {
+ return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { });
+ }
+
+ return null;
+ }
+
+ private static uint SwapBytes(uint x)
+ {
+ x = (x >> 16) | (x << 16);
+
+ return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8);
+ }
+
+ public SharpDX.DirectWrite.Font DWFont { get; }
+
+ public FontFace FontFace { get; }
+
+ public Face Face { get; }
+
+ public HarfBuzzSharp.Font Font { get; }
+
+ ///
+ public short DesignEmHeight { get; }
+
+ ///
+ public int Ascent { get; }
+
+ ///
+ public int Descent { get; }
+
+ ///
+ public int LineGap { get; }
+
+ //ToDo: Read font table for these values
+ ///
+ public int UnderlinePosition { get; }
+
+ ///
+ public int UnderlineThickness { get; }
+
+ ///
+ public int StrikethroughPosition { get; }
+
+ ///
+ public int StrikethroughThickness { get; }
+
+ ///
+ public ushort GetGlyph(uint codepoint)
+ {
+ if (Font.TryGetGlyph(codepoint, out var glyph))
+ {
+ return (ushort)glyph;
+ }
+
+ return 0;
+ }
+
+ ///
+ public ushort[] GetGlyphs(ReadOnlySpan codepoints)
+ {
+ var glyphs = new ushort[codepoints.Length];
+
+ for (var i = 0; i < codepoints.Length; i++)
+ {
+ if (Font.TryGetGlyph(codepoints[i], out var glyph))
+ {
+ glyphs[i] = (ushort)glyph;
+ }
+ }
+
+ return glyphs;
+ }
+
+ ///
+ public int GetGlyphAdvance(ushort glyph)
+ {
+ return Font.GetHorizontalGlyphAdvance(glyph);
+ }
+
+ ///
+ public int[] GetGlyphAdvances(ReadOnlySpan glyphs)
+ {
+ var glyphIndices = new uint[glyphs.Length];
+
+ for (var i = 0; i < glyphs.Length; i++)
+ {
+ glyphIndices[i] = glyphs[i];
+ }
+
+ return Font.GetHorizontalGlyphAdvances(glyphIndices);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+
+ if (!disposing)
+ {
+ return;
+ }
+
+ Font?.Dispose();
+ Face?.Dispose();
+ FontFace?.Dispose();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
+
diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
index e7c379ad89..8f060b1b81 100644
--- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
+++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
@@ -11,19 +11,11 @@ namespace Avalonia.Win32.Input
{
class WindowsMouseDevice : MouseDevice
{
- public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice();
-
public WindowsMouseDevice() : base(new WindowsMousePointer())
{
}
- public WindowImpl CurrentWindow
- {
- get;
- set;
- }
-
class WindowsMousePointer : Pointer
{
public WindowsMousePointer() : base(Pointer.GetNextFreeId(),PointerType.Mouse, true)
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 04a9303d53..2ae200a179 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -30,6 +30,7 @@ namespace Avalonia.Win32
private IntPtr _hwnd;
private bool _multitouch;
private TouchDevice _touchDevice = new TouchDevice();
+ private MouseDevice _mouseDevice = new WindowsMouseDevice();
private IInputRoot _owner;
private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
private bool _trackingMouse;
@@ -205,7 +206,7 @@ namespace Avalonia.Win32
}
}
- public IMouseDevice MouseDevice => WindowsMouseDevice.Instance;
+ public IMouseDevice MouseDevice => _mouseDevice;
public WindowState WindowState
{
@@ -333,7 +334,7 @@ namespace Avalonia.Win32
public void BeginMoveDrag(PointerPressedEventArgs e)
{
- WindowsMouseDevice.Instance.Capture(null);
+ _mouseDevice.Capture(null);
UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
new IntPtr((int)UnmanagedMethods.HitTestValues.HTCAPTION), IntPtr.Zero);
e.Pointer.Capture(null);
@@ -356,7 +357,7 @@ namespace Avalonia.Win32
#if USE_MANAGED_DRAG
_managedDrag.BeginResizeDrag(edge, ScreenToClient(MouseDevice.Position));
#else
- WindowsMouseDevice.Instance.Capture(null);
+ _mouseDevice.Capture(null);
UnmanagedMethods.DefWindowProc(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN,
new IntPtr((int)EdgeDic[edge]), IntPtr.Zero);
#endif
@@ -437,9 +438,7 @@ namespace Avalonia.Win32
uint timestamp = unchecked((uint)UnmanagedMethods.GetMessageTime());
RawInputEventArgs e = null;
-
- WindowsMouseDevice.Instance.CurrentWindow = this;
-
+
switch ((UnmanagedMethods.WindowsMessage)msg)
{
case UnmanagedMethods.WindowsMessage.WM_ACTIVATE:
@@ -485,6 +484,8 @@ namespace Avalonia.Win32
_parent._disabledBy.Remove(this);
_parent.UpdateEnabled();
}
+ _mouseDevice.Dispose();
+ _touchDevice?.Dispose();
//Free other resources
Dispose();
return IntPtr.Zero;
@@ -542,7 +543,7 @@ namespace Avalonia.Win32
if(ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
- WindowsMouseDevice.Instance,
+ _mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN
@@ -559,7 +560,7 @@ namespace Avalonia.Win32
if(ShouldIgnoreTouchEmulatedMessage())
break;
e = new RawPointerEventArgs(
- WindowsMouseDevice.Instance,
+ _mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_LBUTTONUP
@@ -587,7 +588,7 @@ namespace Avalonia.Win32
}
e = new RawPointerEventArgs(
- WindowsMouseDevice.Instance,
+ _mouseDevice,
timestamp,
_owner,
RawPointerEventType.Move,
@@ -597,7 +598,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSEWHEEL:
e = new RawMouseWheelEventArgs(
- WindowsMouseDevice.Instance,
+ _mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
@@ -606,7 +607,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSEHWHEEL:
e = new RawMouseWheelEventArgs(
- WindowsMouseDevice.Instance,
+ _mouseDevice,
timestamp,
_owner,
PointToClient(PointFromLParam(lParam)),
@@ -616,7 +617,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_MOUSELEAVE:
_trackingMouse = false;
e = new RawPointerEventArgs(
- WindowsMouseDevice.Instance,
+ _mouseDevice,
timestamp,
_owner,
RawPointerEventType.LeaveWindow,
@@ -627,7 +628,7 @@ namespace Avalonia.Win32
case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN:
e = new RawPointerEventArgs(
- WindowsMouseDevice.Instance,
+ _mouseDevice,
timestamp,
_owner,
msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN
@@ -649,9 +650,9 @@ namespace Avalonia.Win32
{
Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time,
_owner,
- touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ?
+ touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ?
RawPointerEventType.TouchEnd :
- touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ?
+ touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ?
RawPointerEventType.TouchBegin :
RawPointerEventType.TouchUpdate,
PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)),
@@ -771,11 +772,11 @@ namespace Avalonia.Win32
{
var keys = (UnmanagedMethods.ModifierKeys)ToInt32(wParam);
var modifiers = WindowsKeyboardDevice.Instance.Modifiers;
- if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
+ if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_LBUTTON))
modifiers |= RawInputModifiers.LeftMouseButton;
- if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
+ if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_RBUTTON))
modifiers |= RawInputModifiers.RightMouseButton;
- if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
+ if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
modifiers |= RawInputModifiers.MiddleMouseButton;
return modifiers;
}
@@ -785,7 +786,7 @@ namespace Avalonia.Win32
// Ensure that the delegate doesn't get garbage collected by storing it as a field.
_wndProcDelegate = new UnmanagedMethods.WndProc(WndProc);
- _className = "Avalonia-" + Guid.NewGuid();
+ _className = $"Avalonia-{Guid.NewGuid().ToString()}";
UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX
{
diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
index f1b4b0d071..22f3b4f501 100644
--- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
+++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
@@ -1,14 +1,7 @@
using System;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Avalonia.Animation;
using Avalonia.Controls;
-using Avalonia.Styling;
using Avalonia.UnitTests;
-using Avalonia.Data;
using Xunit;
-using Avalonia.Animation.Easings;
namespace Avalonia.Animation.UnitTests
{
@@ -69,5 +62,26 @@ namespace Avalonia.Animation.UnitTests
Assert.Equal(0, border.Opacity);
}
}
+
+ [Fact]
+ public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick()
+ {
+ var clock = new MockGlobalClock();
+
+ using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
+ {
+ int i = 0;
+ var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue =>
+ {
+ switch (i++)
+ {
+ case 0: Assert.Equal(0, nextValue); break;
+ case 1: Assert.Equal(1d, nextValue); break;
+ }
+ });
+
+ clock.Pulse(TimeSpan.FromMilliseconds(10));
+ }
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
index 2cf1eb1a97..f0e93dbb3a 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
@@ -78,6 +78,18 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(10, target.GetValue(Class1.AttachedProperty));
}
+ [Fact]
+ public void PropertyChanged_Event_Uses_Coerced_Value()
+ {
+ var inst = new Class1();
+ inst.PropertyChanged += (sender, e) =>
+ {
+ Assert.Equal(10, e.NewValue);
+ };
+
+ inst.SetValue(Class1.QuxProperty, 15);
+ }
+
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty QuxProperty =
diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
index 74523d4193..dee7a84812 100644
--- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
@@ -16,7 +16,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Set_ExitCode_After_Shutdown()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.Shutdown(1337);
@@ -31,7 +31,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var windows = new List { new Window(), new Window(), new Window(), new Window() };
@@ -50,7 +50,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Only_Exit_On_Explicit_Exit()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
@@ -84,7 +84,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_MainWindow_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
@@ -112,7 +112,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_Last_Window_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
@@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests
public void Show_Should_Add_Window_To_OpenWindows()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var window = new Window();
@@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests
public void Window_Should_Be_Added_To_OpenWindows_Only_Once()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var window = new Window();
@@ -174,7 +174,7 @@ namespace Avalonia.Controls.UnitTests
public void Close_Should_Remove_Window_From_OpenWindows()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var window = new Window();
@@ -197,7 +197,7 @@ namespace Avalonia.Controls.UnitTests
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object));
using (UnitTestApplication.Start(services))
- using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
+ using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var window = new Window();
diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs
index 2b9197e20b..353bb9c98d 100644
--- a/tests/Avalonia.Controls.UnitTests/GridTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs
@@ -1,11 +1,6 @@
+using System;
using System.Collections.Generic;
using System.Linq;
-using Avalonia.Controls.Primitives;
-using Avalonia.Input;
-using Avalonia.Platform;
-using Avalonia.UnitTests;
-
-using Moq;
using Xunit;
using Xunit.Abstractions;
@@ -34,7 +29,6 @@ namespace Avalonia.Controls.UnitTests
private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns)
{
-
var grid = new Grid();
foreach (var k in columns.Select(c => new ColumnDefinition
{
@@ -1178,6 +1172,41 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth);
}
+ [Fact]
+ public void Size_Group_Definition_Resizes_Are_Tracked()
+ {
+ var grids = new[] {
+ CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (null, new GridLength())),
+ CreateGrid(("A", new GridLength(5, GridUnitType.Pixel)), (null, new GridLength())) };
+ var scope = new Grid();
+ foreach (var xgrids in grids)
+ scope.Children.Add(xgrids);
+
+ var root = new Grid();
+ root.UseLayoutRounding = false;
+ root.SetValue(Grid.IsSharedSizeScopeProperty, true);
+ root.Children.Add(scope);
+
+ root.Measure(new Size(50, 50));
+ root.Arrange(new Rect(new Point(), new Point(50, 50)));
+
+ PrintColumnDefinitions(grids[0]);
+ Assert.Equal(5, grids[0].ColumnDefinitions[0].ActualWidth);
+ Assert.Equal(5, grids[1].ColumnDefinitions[0].ActualWidth);
+
+ grids[0].ColumnDefinitions[0].Width = new GridLength(10, GridUnitType.Pixel);
+
+ foreach (Grid grid in grids)
+ {
+ grid.Measure(new Size(50, 50));
+ grid.Arrange(new Rect(new Point(), new Point(50, 50)));
+ }
+
+ PrintColumnDefinitions(grids[0]);
+ Assert.Equal(10, grids[0].ColumnDefinitions[0].ActualWidth);
+ Assert.Equal(10, grids[1].ColumnDefinitions[0].ActualWidth);
+ }
+
[Fact]
public void Collection_Changes_Are_Tracked()
{
@@ -1270,11 +1299,11 @@ namespace Avalonia.Controls.UnitTests
// grid.Measure(new Size(100, 100));
// grid.Arrange(new Rect(new Point(), new Point(100, 100)));
// PrintColumnDefinitions(grid);
-
+
// NOTE: THIS IS BROKEN IN WPF
// all in group are equal to width (MinWidth) of the sizer in the second column
// Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth));
-
+
// NOTE: THIS IS BROKEN IN WPF
// grid.ColumnDefinitions[2].SharedSizeGroup = null;
@@ -1382,6 +1411,245 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(100, 100), grid.Bounds.Size);
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Changing_Column_Width_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ if (setUsingAvaloniaProperty)
+ grid.ColumnDefinitions[0][ColumnDefinition.WidthProperty] = new GridLength(5);
+ else
+ grid.ColumnDefinitions[0].Width = new GridLength(5);
+ });
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Changing_Column_MinWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ if (setUsingAvaloniaProperty)
+ grid.ColumnDefinitions[0][ColumnDefinition.MinWidthProperty] = 5;
+ else
+ grid.ColumnDefinitions[0].MinWidth = 5;
+ });
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Changing_Column_MaxWidth_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ if (setUsingAvaloniaProperty)
+ grid.ColumnDefinitions[0][ColumnDefinition.MaxWidthProperty] = 5;
+ else
+ grid.ColumnDefinitions[0].MaxWidth = 5;
+ });
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Changing_Row_Height_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
+ {
+ var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ if (setUsingAvaloniaProperty)
+ grid.RowDefinitions[0][RowDefinition.HeightProperty] = new GridLength(5);
+ else
+ grid.RowDefinitions[0].Height = new GridLength(5);
+ });
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Changing_Row_MinHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
+ {
+ var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ if (setUsingAvaloniaProperty)
+ grid.RowDefinitions[0][RowDefinition.MinHeightProperty] = 5;
+ else
+ grid.RowDefinitions[0].MinHeight = 5;
+ });
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Changing_Row_MaxHeight_Should_Invalidate_Grid(bool setUsingAvaloniaProperty)
+ {
+ var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ if (setUsingAvaloniaProperty)
+ grid.RowDefinitions[0][RowDefinition.MaxHeightProperty] = 5;
+ else
+ grid.RowDefinitions[0].MaxHeight = 5;
+ });
+ }
+
+ [Fact]
+ public void Adding_Column_Should_Invalidate_Grid()
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ grid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(5)));
+ });
+ }
+
+ [Fact]
+ public void Adding_Row_Should_Invalidate_Grid()
+ {
+ var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ grid.RowDefinitions.Add(new RowDefinition(new GridLength(5)));
+ });
+ }
+
+ [Fact]
+ public void Replacing_Columns_Should_Invalidate_Grid()
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ grid.ColumnDefinitions = ColumnDefinitions.Parse("2*,1*");
+ });
+ }
+
+ [Fact]
+ public void Replacing_Rows_Should_Invalidate_Grid()
+ {
+ var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ grid.RowDefinitions = RowDefinitions.Parse("2*,1*");
+ });
+ }
+
+ [Fact]
+ public void Removing_Column_Should_Invalidate_Grid()
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ grid.ColumnDefinitions.RemoveAt(0);
+ });
+ }
+
+ [Fact]
+ public void Removing_Row_Should_Invalidate_Grid()
+ {
+ var grid = new Grid { RowDefinitions = RowDefinitions.Parse("1*,1*") };
+
+ Change_Property_And_Verify_Measure_Requested(grid, () =>
+ {
+ grid.RowDefinitions.RemoveAt(0);
+ });
+ }
+
+ [Fact]
+ public void Removing_Child_Should_Invalidate_Grid_And_Be_Operational()
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") };
+
+ grid.Children.Add(new Decorator() { [Grid.ColumnProperty] = 0 });
+ grid.Children.Add(new Decorator() { Width = 10, Height = 10, [Grid.ColumnProperty] = 1 });
+
+ var size = new Size(100, 100);
+ grid.Measure(size);
+ grid.Arrange(new Rect(size));
+
+ Assert.True(grid.IsMeasureValid);
+ Assert.True(grid.IsArrangeValid);
+
+ Assert.Equal(90, grid.Children[0].Bounds.Width);
+ Assert.Equal(10, grid.Children[1].Bounds.Width);
+
+ grid.Children.RemoveAt(1);
+
+ Assert.False(grid.IsMeasureValid);
+ Assert.False(grid.IsArrangeValid);
+
+ grid.Measure(size);
+ grid.Arrange(new Rect(size));
+
+ Assert.True(grid.IsMeasureValid);
+ Assert.True(grid.IsArrangeValid);
+
+ Assert.Equal(100, grid.Children[0].Bounds.Width);
+ }
+
+ [Fact]
+ public void Adding_Child_Should_Invalidate_Grid_And_Be_Operational()
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("*,Auto") };
+
+ grid.Children.Add(new Decorator() { [Grid.ColumnProperty] = 0 });
+
+ var size = new Size(100, 100);
+ grid.Measure(size);
+ grid.Arrange(new Rect(size));
+
+ Assert.True(grid.IsMeasureValid);
+ Assert.True(grid.IsArrangeValid);
+
+ Assert.Equal(100, grid.Children[0].Bounds.Width);
+
+ grid.Children.Add(new Decorator() { Width = 10, Height = 10, [Grid.ColumnProperty] = 1 });
+
+ Assert.False(grid.IsMeasureValid);
+ Assert.False(grid.IsArrangeValid);
+
+ grid.Measure(size);
+ grid.Arrange(new Rect(size));
+
+ Assert.True(grid.IsMeasureValid);
+ Assert.True(grid.IsArrangeValid);
+
+ Assert.Equal(90, grid.Children[0].Bounds.Width);
+ Assert.Equal(10, grid.Children[1].Bounds.Width);
+ }
+
+ private static void Change_Property_And_Verify_Measure_Requested(Grid grid, Action change)
+ {
+ grid.Measure(new Size(100, 100));
+ grid.Arrange(new Rect(grid.DesiredSize));
+
+ Assert.True(grid.IsMeasureValid);
+ Assert.True(grid.IsArrangeValid);
+
+ change();
+
+ Assert.False(grid.IsMeasureValid);
+ Assert.False(grid.IsArrangeValid);
+ }
+
private class TestControl : Control
{
public Size MeasureSize { get; set; }
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index b2839360ee..227d783874 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -12,6 +12,7 @@ using Xunit;
using System.Collections.ObjectModel;
using Avalonia.UnitTests;
using Avalonia.Input;
+using System.Collections.Generic;
namespace Avalonia.Controls.UnitTests
{
@@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { child }, target.GetLogicalChildren());
}
+ [Fact]
+ public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl()
+ {
+ var item = new Border();
+ var items = new ObservableCollection();
+
+ var target = new ItemsControl
+ {
+ Template = GetTemplate(),
+ Items = items,
+ };
+
+ var root = new TestRoot(true, target);
+
+ root.Measure(new Size(100, 100));
+ root.Arrange(new Rect(0, 0, 100, 100));
+
+ items.Add(item);
+
+ Assert.Equal(target, item.Parent);
+ }
+
[Fact]
public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate()
{
@@ -522,6 +545,36 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void Presenter_Items_Should_Be_In_Sync()
+ {
+ var target = new ItemsControl
+ {
+ Template = GetTemplate(),
+ Items = new object[]
+ {
+ new Button(),
+ new Button(),
+ },
+ };
+
+ var root = new TestRoot { Child = target };
+ var otherPanel = new StackPanel();
+
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ target.ItemContainerGenerator.Materialized += (s, e) =>
+ {
+ Assert.IsType