From f2c3805d6a0086ce27ac9ecb55680def9f31b9ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Feb 2023 18:08:13 +0100 Subject: [PATCH 01/11] Update ncrunch config. --- .ncrunch/Avalonia.UnitTests.v3.ncrunchproject | 5 +++++ .ncrunch/GpuInterop.v3.ncrunchproject | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .ncrunch/Avalonia.UnitTests.v3.ncrunchproject create mode 100644 .ncrunch/GpuInterop.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 1e926dac72b4c666d4c213cd72693d4cbf551e27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 1 Feb 2023 18:19:40 +0100 Subject: [PATCH 02/11] Added failing tests for #10110. --- .../AvaloniaObjectTests_Binding.cs | 18 +++++++++++++ .../Xaml/ResourceDictionaryTests.cs | 25 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 030d6ba215..f0386615c5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -1285,6 +1285,24 @@ namespace Avalonia.Base.UnitTests subscription.Dispose(); } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Binding_Producing_UnsetValue_Does_Not_Cause_Unsubscribe(BindingPriority priority) + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source, priority); + + source.OnNext("foo"); + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + source.OnNext(BindingValue.Unset); + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + source.OnNext("bar"); + Assert.Equal("bar", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Produces_Correct_Values_And_Base_Values_With_Multiple_Animation_Bindings() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index 6cab83751f..bea6bc4dc8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs @@ -307,7 +307,30 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.True(buttonResources.ContainsDeferredKey("Red")); } } - + + [Fact] + public void Dynamically_Changing_Referenced_Resources_Works_With_DynamicResource() + { + var xaml = @" + + + Red + + +"; + + var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + + userControl.Resources.Remove("color"); + Assert.Equal(default, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + + userControl.Resources.Add("color", Colors.Blue); + Assert.Equal(Colors.Blue, ((ISolidColorBrush)userControl.FindResource("brush")!).Color); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( From 7b00ef69892358650c10e0f27fb0d5dd1477e3f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 12:00:40 +0100 Subject: [PATCH 03/11] Make untyped bindings produce default value. This changes the behavior of bindings slightly, in that previously a binding which produced `UnsetValue` (or a binding error) caused the value of any lower priority value of value to take effect. After this change, the binding reverts to the property's default value. This behavior more closely matches WPF and fixes #10110. --- src/Avalonia.Base/AvaloniaObject.cs | 4 +- .../PropertyStore/BindingEntryBase.cs | 47 +++++++++---------- .../LocalValueBindingObserver.cs | 42 ++++++++++++----- .../LocalValueUntypedBindingObserver.cs | 25 +++++++--- .../SourceUntypedBindingEntry.cs | 2 + .../PropertyStore/TypedBindingEntry.cs | 2 + .../PropertyStore/UntypedBindingEntry.cs | 5 ++ .../AvaloniaObjectTests_Binding.cs | 18 ++++++- .../AvaloniaObjectTests_Validation.cs | 4 +- 9 files changed, 99 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 1946d4ba5c..db6dbe98e0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -270,8 +270,8 @@ namespace Avalonia /// The property. /// True if the property is set, otherwise false. /// - /// Checks whether a value is assigned to the property, or that there is a binding to the - /// property that is producing a value other than . + /// Returns true if is a styled property which has a value + /// assigned to it or a binding targeting it; otherwise false. /// public bool IsSet(AvaloniaProperty property) { diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index 11dc80ef8f..e1ff0970c2 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -16,6 +16,8 @@ namespace Avalonia.PropertyStore private IDisposable? _subscription; private bool _hasValue; private TValue? _value; + private TValue? _defaultValue; + private bool _isDefaultValueInitialized; protected BindingEntryBase( ValueFrame frame, @@ -89,6 +91,7 @@ namespace Avalonia.PropertyStore protected abstract BindingValue ConvertAndValidate(TSource value); protected abstract BindingValue ConvertAndValidate(BindingValue value); + protected abstract TValue GetDefaultValue(Type ownerType); protected virtual void Start(bool produceValue) { @@ -104,17 +107,6 @@ namespace Avalonia.PropertyStore }; } - private void ClearValue() - { - if (_hasValue) - { - _hasValue = false; - _value = default; - if (_subscription is not null) - Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); - } - } - private void SetValue(BindingValue value) { static void Execute(BindingEntryBase instance, BindingValue value) @@ -124,24 +116,20 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); - if (value.HasValue) - { - if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)) - { - instance._value = value.Value; - instance._hasValue = true; - if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); - } - } - else if (value.Type != BindingValueType.DoNothing) + var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + + if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, effectiveValue)) { - instance.ClearValue(); + instance._value = effectiveValue; + instance._hasValue = true; if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority); + instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); } } + if (value.Type == BindingValueType.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -161,5 +149,16 @@ namespace Avalonia.PropertyStore _subscription = null; Frame.OnBindingCompleted(this); } + + private TValue GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index f89cb029b6..0771ade647 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -10,6 +10,8 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; public LocalValueBindingObserver(ValueStore owner, StyledProperty property) { @@ -41,26 +43,28 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - static void Execute(ValueStore owner, StyledProperty property, T value) + static void Execute(LocalValueBindingObserver instance, T value) { - if (property.ValidateValue?.Invoke(value) != false) - owner.SetValue(property, value, BindingPriority.LocalValue); - else - owner.ClearLocalValue(property); + var owner = instance._owner; + var property = instance.Property; + + if (property.ValidateValue?.Invoke(value) == false) + value = instance.GetCachedDefaultValue(); + + owner.SetValue(property, value, BindingPriority.LocalValue); } if (Dispatcher.UIThread.CheckAccess()) { - Execute(_owner, Property, value); + Execute(this, value); } else { // To avoid allocating closure in the outer scope we need to capture variables // locally. This allows us to skip most of the allocations when on UI thread. - var instance = _owner; - var property = Property; + var instance = this; var newValue = value; - Dispatcher.UIThread.Post(() => Execute(instance, property, newValue)); + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } @@ -73,12 +77,13 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(owner.Owner, property, value); - if (value.HasValue) - owner.SetValue(property, value.Value, BindingPriority.LocalValue); - else if (value.Type != BindingValueType.DataValidationError) - owner.ClearLocalValue(property); + var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); } + if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -92,5 +97,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 2d157b2519..46e6ed810a 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,5 +1,4 @@ using System; -using System.Security.Cryptography; using Avalonia.Data; using Avalonia.Threading; @@ -10,6 +9,8 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property) { @@ -49,11 +50,7 @@ namespace Avalonia.PropertyStore if (value == AvaloniaProperty.UnsetValue) { - owner.ClearLocalValue(property); - } - else if (value == BindingOperations.DoNothing) - { - // Do nothing! + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); } else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) { @@ -61,11 +58,14 @@ namespace Avalonia.PropertyStore } else { - owner.ClearLocalValue(property); + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); } } + if (value == BindingOperations.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -79,5 +79,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs index b56d0d4529..b82714817b 100644 --- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -31,5 +31,7 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override TTarget GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs index 697725c87b..550f5c0001 100644 --- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -48,5 +48,7 @@ namespace Avalonia.PropertyStore return value; } + + protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs index f8becb2e06..a77d7fddb6 100644 --- a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs @@ -29,5 +29,10 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override object? GetDefaultValue(Type ownerType) + { + return ((IStyledPropertyMetadata)Property.GetMetadata(ownerType)).DefaultValue; + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index f0386615c5..baaed5104d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -372,6 +372,20 @@ namespace Avalonia.Base.UnitTests Assert.Null(target.GetValue(property)); } + [Fact] + public void LocalValue_Bind_Generic_To_ValueType_Accepts_UnsetValue() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(BindingValue.Unset); + + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); + } + [Fact] public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue() { @@ -383,7 +397,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); - Assert.False(target.IsSet(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); } [Fact] @@ -397,7 +411,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(AvaloniaProperty.UnsetValue); Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); - Assert.False(target.IsSet(Class1.QuxProperty)); + Assert.True(target.IsSet(Class1.QuxProperty)); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index e8175cf477..3fc950573d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests } [Fact] - public void Reverts_To_Lower_Priority_If_Style_Binding_Fails_Validation() + public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation_2() { var target = new Class1(); var source = new Subject(); @@ -75,7 +75,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.FooProperty, source, BindingPriority.StyleTrigger); source.OnNext(150); - Assert.Equal(10, target.GetValue(Class1.FooProperty)); + Assert.Equal(11, target.GetValue(Class1.FooProperty)); } [Fact] From bca57ea0e9c65c5b17422303261b1a7a058d842b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 16:27:48 +0100 Subject: [PATCH 04/11] Added additional failing test. --- .../AvaloniaObjectTests_Validation.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs index 3fc950573d..513aeb65ab 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs @@ -78,6 +78,20 @@ namespace Avalonia.Base.UnitTests Assert.Equal(11, target.GetValue(Class1.FooProperty)); } + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation_3(BindingPriority priority) + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source, priority); + source.OnNext(150); + + Assert.Equal(11, target.GetValue(Class1.FooProperty)); + } + [Fact] public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings() { From b2c1d80f5cfdf3c0dccbed07cb908b69f7db3e2d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Feb 2023 16:32:02 +0100 Subject: [PATCH 05/11] Fix validation of local value bindings. Local value bindings with a type of `BindingValue` were not being validated. --- .../PropertyStore/LocalValueBindingObserver.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 0771ade647..5908d9e535 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -77,8 +77,17 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(owner.Owner, property, value); - var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); - owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); + if (value.HasValue) + { + var effectiveValue = value.Value; + if (property.ValidateValue?.Invoke(effectiveValue) == false) + effectiveValue = instance.GetCachedDefaultValue(); + owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); + } + else + { + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); + } } if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) From 467ef300bf559123f3dc355185ad78efdf0675ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Feb 2023 10:43:26 +0100 Subject: [PATCH 06/11] Remove unneeded method. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index f36a96992b..a758360545 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -380,23 +380,6 @@ namespace Avalonia.PropertyStore } } - /// - /// Called by non-LocalValue binding entries to re-evaluate the effective value when the - /// binding produces an unset value. - /// - /// The bound property. - /// The priority of binding which produced a new value. - public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority) - { - Debug.Assert(priority != BindingPriority.LocalValue); - - if (TryGetEffectiveValue(property, out var existing)) - { - if (priority <= existing.Priority) - ReevaluateEffectiveValue(property, existing); - } - } - /// /// Called by a when its /// state changes. From 2c0c03d317c358add598c6406ea4c6d33f97aaee Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 01:16:47 +0100 Subject: [PATCH 07/11] Fix CustomDrawingExampleControl crash on Direct2D --- .../Pages/CustomDrawingExampleControl.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index 11e5e32cf1..938c45a4e2 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -59,10 +59,12 @@ namespace ControlCatalog.Pages }; StreamGeometry sg = new StreamGeometry(); - var cntx = sg.Open(); - cntx.BeginFigure(new Point(-25.0d, -10.0d), false); - cntx.ArcTo(new Point(25.0d, -10.0d), new Size(10.0d, 10.0d), 0.0d, false, SweepDirection.Clockwise); - cntx.EndFigure(true); + using (var cntx = sg.Open()) + { + cntx.BeginFigure(new Point(-25.0d, -10.0d), false); + cntx.ArcTo(new Point(25.0d, -10.0d), new Size(10.0d, 10.0d), 0.0d, false, SweepDirection.Clockwise); + cntx.EndFigure(true); + } _smileGeometry = sg.Clone(); } From 3568e60841a4cd2229a1a1b22d8dd1cfa4392d1d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 01:20:47 +0100 Subject: [PATCH 08/11] Set WindowsInteropTest platform target to x64 --- .../interop/WindowsInteropTest/WindowsInteropTest.csproj | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 1643ca3ee2..95f77f6df9 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -2,7 +2,7 @@ WinExe net461 - + x64 true true @@ -10,9 +10,6 @@ - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - + From 7d879cf6febe500a6f5f7fcda786b88d075abbf0 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 13:00:25 +0100 Subject: [PATCH 09/11] Added Win32PlatformOptions.ShouldRenderOnUIThread Fixes WPF interop --- samples/interop/WindowsInteropTest/Program.cs | 11 +++++++-- .../Rendering/DefaultRenderTimer.cs | 5 ++-- src/Avalonia.Base/Rendering/IRenderLoop.cs | 5 +++- src/Avalonia.Base/Rendering/RenderLoop.cs | 1 + .../Rendering/UiThreadRenderTimer.cs | 9 ++++++- .../Wpf/WpfTopLevelImpl.cs | 9 +------ src/Windows/Avalonia.Win32/Win32Platform.cs | 24 +++++++++++++------ 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/samples/interop/WindowsInteropTest/Program.cs b/samples/interop/WindowsInteropTest/Program.cs index fac06d74b0..c2d30c67bb 100644 --- a/samples/interop/WindowsInteropTest/Program.cs +++ b/samples/interop/WindowsInteropTest/Program.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Controls; using ControlCatalog; using Avalonia; @@ -15,7 +14,15 @@ namespace WindowsInteropTest { System.Windows.Forms.Application.EnableVisualStyles(); System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); - AppBuilder.Configure().UseWin32().UseDirect2D1().SetupWithoutStarting(); + AppBuilder.Configure() + .UseWin32() + .UseDirect2D1() + .With(new Win32PlatformOptions + { + UseWindowsUIComposition = false, + ShouldRenderOnUIThread = true // necessary for WPF + }) + .SetupWithoutStarting(); System.Windows.Forms.Application.Run(new SelectorForm()); } } diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index d0d3dd9715..7b0fecf675 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics; -using System.Threading.Tasks; using Avalonia.Platform; namespace Avalonia.Rendering @@ -59,7 +57,8 @@ namespace Avalonia.Rendering } } - public bool RunsInBackground => true; + /// + public virtual bool RunsInBackground => true; /// /// Starts the timer. diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index e500ecdf8b..ebe683949d 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -27,7 +27,10 @@ namespace Avalonia.Rendering /// /// The update task. void Remove(IRenderLoopTask i); - + + /// + /// Indicates if the rendering is done on a non-UI thread. + /// bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 1f58ca3827..185f44d29a 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -87,6 +87,7 @@ namespace Avalonia.Rendering } } + /// public bool RunsInBackground => Timer.RunsInBackground; private void TimerTick(TimeSpan time) diff --git a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs index 1bbf804b5f..7f2eedc98c 100644 --- a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs @@ -8,13 +8,20 @@ namespace Avalonia.Rendering /// /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms /// - public class UiThreadRenderTimer : DefaultRenderTimer { + /// + /// Initializes a new instance of the class. + /// + /// The number of frames per second at which the loop should run. public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) { } + /// + public override bool RunsInBackground => false; + + /// protected override IDisposable StartCore(Action tick) { bool cancelled = false; diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 2d0f351d58..13eae1992c 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -45,13 +45,6 @@ namespace Avalonia.Win32.Interop.Wpf ((FrameworkElement)PlatformImpl)?.InvalidateMeasure(); } - protected override void HandleResized(Size clientSize, PlatformResizeReason reason) - { - ClientSize = clientSize; - LayoutManager.ExecuteLayoutPass(); - Renderer?.Resized(clientSize); - } - public Size AllocatedSize => ClientSize; } @@ -223,7 +216,7 @@ namespace Avalonia.Win32.Interop.Wpf (Key)e.Key, GetModifiers(null))); - protected override void OnTextInput(TextCompositionEventArgs e) + protected override void OnTextInput(TextCompositionEventArgs e) => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); void ITopLevelImpl.SetCursor(ICursorImpl cursor) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index f16a1ca8cf..c34be9008a 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -6,12 +6,10 @@ using System.IO; using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading; -using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.Media; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; @@ -20,7 +18,6 @@ using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; -using Avalonia.Win32.WinRT; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia @@ -68,6 +65,7 @@ namespace Avalonia /// /// Render Avalonia to a Texture inside the Windows.UI.Composition tree. + /// This setting is true by default. /// /// /// Supported on Windows 10 build 16299 and above. Ignored on other versions. @@ -88,9 +86,19 @@ namespace Avalonia /// This is only recommended if low input latency is desirable, and there is no need for the transparency /// and stylings / blurrings offered by
/// This is mutually exclusive with - /// which if active will override this setting. + /// which if active will override this setting. + /// This setting is false by default. ///
- public bool UseLowLatencyDxgiSwapChain { get; set; } = false; + public bool UseLowLatencyDxgiSwapChain { get; set; } + + /// + /// Render directly on the UI thread instead of using a dedicated render thread. + /// Only applicable if both and + /// are false. + /// This setting is only recommended for interop with systems that must render on the UI thread, such as WPF. + /// This setting is false by default. + /// + public bool ShouldRenderOnUIThread { get; set; } /// /// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu @@ -128,7 +136,7 @@ namespace Avalonia.Win32 internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } - + internal static Compositor Compositor { get; private set; } public static void Initialize() @@ -139,6 +147,8 @@ namespace Avalonia.Win32 public static void Initialize(Win32PlatformOptions options) { Options = options; + var renderTimer = options.ShouldRenderOnUIThread ? new UiThreadRenderTimer(60) : new DefaultRenderTimer(60); + AvaloniaLocator.CurrentMutable .Bind().ToSingleton() .Bind().ToConstant(CursorFactory.Instance) @@ -146,7 +156,7 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(renderTimer) .Bind().ToConstant(s_instance) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control) { From 1c9032dd7228ae2c2ae520dae690f0851fda1a33 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 10 Feb 2023 13:04:14 +0100 Subject: [PATCH 10/11] Win32 tray icon: avoid HIcon access when removing Prevents a crash when the TrayIconImpl finalizer runs after the System.Drawing.Icon has been disposed --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index ab70d77a09..fe920ff1a1 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -81,19 +81,18 @@ namespace Avalonia.Win32 private void UpdateIcon(bool remove = false) { - var iconData = new NOTIFYICONDATA() + var iconData = new NOTIFYICONDATA { hWnd = Win32Platform.Instance.Handle, - uID = _uniqueId, - uFlags = NIF.TIP | NIF.MESSAGE, - uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, - hIcon = _icon?.HIcon ?? s_emptyIcon, - szTip = _tooltipText ?? "" + uID = _uniqueId }; if (!remove) { - iconData.uFlags |= NIF.ICON; + iconData.uFlags = NIF.TIP | NIF.MESSAGE | NIF.ICON; + iconData.uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE; + iconData.hIcon = _icon?.HIcon ?? s_emptyIcon; + iconData.szTip = _tooltipText ?? ""; if (!_iconAdded) { @@ -107,6 +106,7 @@ namespace Avalonia.Win32 } else { + iconData.uFlags = 0; Shell_NotifyIcon(NIM.DELETE, iconData); _iconAdded = false; } From 23d999c2eb1d5ce39e42d1c8ce36243d7d30d47a Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Fri, 10 Feb 2023 19:18:18 +0200 Subject: [PATCH 11/11] Make all Render overrides sealed (#10299) --- src/Avalonia.Controls/Border.cs | 2 +- src/Avalonia.Controls/ExperimentalAcrylicBorder.cs | 2 +- src/Avalonia.Controls/Grid.cs | 2 +- src/Avalonia.Controls/Image.cs | 2 +- src/Avalonia.Controls/Panel.cs | 2 +- src/Avalonia.Controls/Presenters/ContentPresenter.cs | 2 +- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- src/Avalonia.Controls/Primitives/AccessText.cs | 5 ++--- src/Avalonia.Controls/Remote/RemoteWidget.cs | 2 +- src/Avalonia.Controls/Shapes/Shape.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 8 +++++++- src/Avalonia.Controls/TickBar.cs | 2 +- 12 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 1bb574acd2..78ba23c1dd 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -225,7 +225,7 @@ namespace Avalonia.Controls /// Renders the control. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { _borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray); diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index e4487d29fa..e1f840672d 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls _subscription?.Dispose(); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) { diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 2501440ff2..ff9fb4e31d 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -3256,7 +3256,7 @@ namespace Avalonia.Controls /// /// UpdateRenderBounds. /// - public override void Render(DrawingContext drawingContext) + public sealed override void Render(DrawingContext drawingContext) { var grid = this.GetVisualParent(); diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 2cf0fc3ec4..3e76835e92 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls /// Renders the control. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var source = Source; diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 007d18c813..a7dc035459 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls /// Renders the visual to a . /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var background = Background; if (background != null) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 584dfea97f..be61bb18a1 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -534,7 +534,7 @@ namespace Avalonia.Controls.Presenters } /// - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { _borderRenderer.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow); diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index f599511392..3481b1ecf3 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -388,7 +388,7 @@ namespace Avalonia.Controls.Presenters TextLayout.Draw(context, new Point(left, top)); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 49e76d0728..ed3412bb45 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -60,10 +60,9 @@ namespace Avalonia.Controls.Primitives /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(DrawingContext context) + protected internal override void RenderCore(DrawingContext context) { - base.Render(context); - + base.RenderCore(context); int underscore = Text?.IndexOf('_') ?? -1; if (underscore != -1 && ShowAccessKey) diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 5cefb0d89f..b9854ab837 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -69,7 +69,7 @@ namespace Avalonia.Controls.Remote base.ArrangeCore(finalRect); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { if (_lastFrame != null && _lastFrame.Width != 0 && _lastFrame.Height != 0) { diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index c8bf95b3f7..461dc1c947 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -193,7 +193,7 @@ namespace Avalonia.Controls.Shapes set { SetValue(StrokeJoinProperty, value); } } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var geometry = RenderedGeometry; diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index ec31470126..df98d1073e 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -549,7 +549,13 @@ namespace Avalonia.Controls /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) + { + RenderCore(context); + } + + // Workaround to seal Render method, we need to make so because AccessText was overriding Render method which is sealed now. + internal protected virtual void RenderCore(DrawingContext context) { var background = Background; diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 4d902d3d5f..63fe9c9384 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -213,7 +213,7 @@ namespace Avalonia.Controls /// /// Brush that use to fill ticks is specified by Fill property. /// - public override void Render(DrawingContext dc) + public sealed override void Render(DrawingContext dc) { var size = new Size(Bounds.Width, Bounds.Height); var range = Maximum - Minimum;