From 01edb8c8b128318a3c12d85be433af429dd1639f Mon Sep 17 00:00:00 2001 From: Adir Date: Wed, 7 Oct 2020 19:25:18 +0300 Subject: [PATCH 001/327] Added potential fix for hotkey manager instances not clearing up after detaching from root --- src/Avalonia.Controls/HotkeyManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 95752e7875..9a2b4f2d0a 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -55,6 +55,11 @@ namespace Avalonia.Controls private void OnParentChanged(TopLevel control) { + if (control == null) + { + Stop(); + return; + } Unregister(); _root = control; Register(); From 188192c1259c34abcbcaaca1f5c4ede0d7259850 Mon Sep 17 00:00:00 2001 From: Adir Date: Wed, 7 Oct 2020 20:57:41 +0300 Subject: [PATCH 002/327] Changed HotkeyManager.cs to stop/init based on detaching/attaching to visual tree --- src/Avalonia.Controls/HotkeyManager.cs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 9a2b4f2d0a..bff25d6dad 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -2,6 +2,7 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.LogicalTree; namespace Avalonia.Controls { @@ -45,6 +46,8 @@ namespace Avalonia.Controls { _control = control; _wrapper = new HotkeyCommandWrapper(_control); + _control.DetachedFromVisualTree += ControlOnDetachedFromVisualTree; + _control.AttachedToLogicalTree += ControlOnAttachedToLogicalTree; } public void Init() @@ -53,13 +56,19 @@ namespace Avalonia.Controls _parentSub = AncestorFinder.Create(_control).Subscribe(OnParentChanged); } + private void ControlOnAttachedToLogicalTree(object sender, LogicalTreeAttachmentEventArgs e) + { + Stop(); + Init(); + } + + private void ControlOnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + Stop(); + } + private void OnParentChanged(TopLevel control) { - if (control == null) - { - Stop(); - return; - } Unregister(); _root = control; Register(); @@ -89,7 +98,7 @@ namespace Avalonia.Controls { if (_root != null && _hotkey != null) { - _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; + _binding = new KeyBinding() { Gesture = _hotkey, Command = _wrapper }; _root.KeyBindings.Add(_binding); } } @@ -107,11 +116,12 @@ namespace Avalonia.Controls HotKeyProperty.Changed.Subscribe(args => { var control = args.Sender as IControl; - if (args.OldValue != null|| control == null) + if (args.OldValue != null || control == null) return; new Manager(control).Init(); }); } + public static void SetHotKey(AvaloniaObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value); public static KeyGesture GetHotKey(AvaloniaObject target) => target.GetValue(HotKeyProperty); } From ecba5f65f05da4917b6a454e81abf3f99b76ceb4 Mon Sep 17 00:00:00 2001 From: Adir Date: Wed, 7 Oct 2020 23:11:50 +0300 Subject: [PATCH 003/327] Reverted HotkeyManager.cs Changed Button.cs to remove it's HotKey property when detaching from visual tree --- src/Avalonia.Controls/Button.cs | 12 ++++++++++-- src/Avalonia.Controls/HotkeyManager.cs | 14 -------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index e94d00b2ff..c541a384b0 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -80,6 +80,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; + private KeyGesture _hotkey; /// /// Initializes static members of the class. @@ -168,11 +169,13 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { + if (_hotkey != null) + HotKey = _hotkey; base.OnAttachedToVisualTree(e); if (IsDefault) @@ -182,6 +185,7 @@ namespace Avalonia.Controls ListenForDefault(inputElement); } } + if (IsCancel) { if (e.Root is IInputElement inputElement) @@ -194,6 +198,8 @@ namespace Avalonia.Controls /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { + _hotkey = HotKey; + HotKey = null; base.OnDetachedFromVisualTree(e); if (IsDefault) @@ -239,6 +245,7 @@ namespace Avalonia.Controls { OnClick(); } + IsPressed = true; e.Handled = true; } @@ -255,6 +262,7 @@ namespace Avalonia.Controls { OnClick(); } + IsPressed = false; e.Handled = true; } @@ -309,7 +317,7 @@ namespace Avalonia.Controls } } } - + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index bff25d6dad..5972a0fdd9 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -2,7 +2,6 @@ using System; using System.Windows.Input; using Avalonia.Controls.Utils; using Avalonia.Input; -using Avalonia.LogicalTree; namespace Avalonia.Controls { @@ -46,8 +45,6 @@ namespace Avalonia.Controls { _control = control; _wrapper = new HotkeyCommandWrapper(_control); - _control.DetachedFromVisualTree += ControlOnDetachedFromVisualTree; - _control.AttachedToLogicalTree += ControlOnAttachedToLogicalTree; } public void Init() @@ -56,17 +53,6 @@ namespace Avalonia.Controls _parentSub = AncestorFinder.Create(_control).Subscribe(OnParentChanged); } - private void ControlOnAttachedToLogicalTree(object sender, LogicalTreeAttachmentEventArgs e) - { - Stop(); - Init(); - } - - private void ControlOnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - Stop(); - } - private void OnParentChanged(TopLevel control) { Unregister(); From 7c952cfc5fb3063b03b3860d7faf1b69e6db1b46 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 16:33:32 +0300 Subject: [PATCH 004/327] Changed Hotkey reset to detatch from logical tree (was visual tree) --- src/Avalonia.Controls/Button.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c541a384b0..046253b3cd 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -174,8 +174,6 @@ namespace Avalonia.Controls /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - if (_hotkey != null) - HotKey = _hotkey; base.OnAttachedToVisualTree(e); if (IsDefault) @@ -198,8 +196,6 @@ namespace Avalonia.Controls /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { - _hotkey = HotKey; - HotKey = null; base.OnDetachedFromVisualTree(e); if (IsDefault) @@ -213,6 +209,8 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (_hotkey != null) + HotKey = _hotkey; base.OnAttachedToLogicalTree(e); if (Command != null) @@ -223,6 +221,12 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (HotKey != null) + { + _hotkey = HotKey; + HotKey = null; + } + base.OnDetachedFromLogicalTree(e); if (Command != null) From 3c302f72f236a94d5b1d3f31fcab52d351d31625 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 18:03:52 +0300 Subject: [PATCH 005/327] Added docs explaining why need to change Hotkey on attach/detach from logical tree Added the same logic to MenuItem.cs Reverted auto indentation in Button.cs --- src/Avalonia.Controls/Button.cs | 9 ++++----- src/Avalonia.Controls/MenuItem.cs | 11 +++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 046253b3cd..87fabf5ed7 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -169,7 +169,7 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } - protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -183,7 +183,6 @@ namespace Avalonia.Controls ListenForDefault(inputElement); } } - if (IsCancel) { if (e.Root is IInputElement inputElement) @@ -210,6 +209,7 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_hotkey != null) + // Control attached again, set Hotkey to create a hotkey manager for this control HotKey = _hotkey; base.OnAttachedToLogicalTree(e); @@ -223,6 +223,7 @@ namespace Avalonia.Controls { if (HotKey != null) { + // This will cause the hotkey manager to dispose the observer and the reference to this control _hotkey = HotKey; HotKey = null; } @@ -249,7 +250,6 @@ namespace Avalonia.Controls { OnClick(); } - IsPressed = true; e.Handled = true; } @@ -266,7 +266,6 @@ namespace Avalonia.Controls { OnClick(); } - IsPressed = false; e.Handled = true; } @@ -321,7 +320,7 @@ namespace Avalonia.Controls } } } - + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) { IsPressed = false; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 7d4fef009d..faf92c97fe 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -102,6 +102,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; private Popup? _popup; + private KeyGesture _hotkey; /// /// Initializes static members of the class. @@ -337,6 +338,9 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (_hotkey != null) + // Control attached again, set Hotkey to create a hotkey manager for this control + HotKey = _hotkey; base.OnAttachedToLogicalTree(e); if (Command != null) @@ -347,6 +351,13 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + if (HotKey != null) + { + // This will cause the hotkey manager to dispose the observer and the reference to this control + _hotkey = HotKey; + HotKey = null; + } + base.OnDetachedFromLogicalTree(e); if (Command != null) From 68f67f703401c50105ed63250266f44ec7f190a7 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 18:05:46 +0300 Subject: [PATCH 006/327] Reverted auto indentation in HotkeyManager.cs --- src/Avalonia.Controls/HotkeyManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index 5972a0fdd9..95752e7875 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -84,7 +84,7 @@ namespace Avalonia.Controls { if (_root != null && _hotkey != null) { - _binding = new KeyBinding() { Gesture = _hotkey, Command = _wrapper }; + _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; _root.KeyBindings.Add(_binding); } } @@ -102,12 +102,11 @@ namespace Avalonia.Controls HotKeyProperty.Changed.Subscribe(args => { var control = args.Sender as IControl; - if (args.OldValue != null || control == null) + if (args.OldValue != null|| control == null) return; new Manager(control).Init(); }); } - public static void SetHotKey(AvaloniaObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value); public static KeyGesture GetHotKey(AvaloniaObject target) => target.GetValue(HotKeyProperty); } From fd2da7aa49b2ba59289d205e35b39cff177b43db Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 8 Oct 2020 21:26:21 +0300 Subject: [PATCH 007/327] Added unit tests for enforcing manager releasing reference to the controls Changed AncestorFinder.cs to dispose child nodes and subject (need review on this) --- src/Avalonia.Controls/Utils/AncestorFinder.cs | 2 + .../Utils/HotKeyManagerTests.cs | 113 +++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index 529aec1393..42e0d85dcd 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -47,6 +47,8 @@ namespace Avalonia.Controls.Utils public void Dispose() { + _child?.Dispose(); + _subject.Dispose(); _disposable.Dispose(); } } diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 9f2712c93c..6425c5c8f4 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -1,9 +1,14 @@ -using Moq; +using System; +using System.Collections.Generic; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Utils @@ -50,10 +55,116 @@ namespace Avalonia.Controls.UnitTests.Utils HotKeyManager.SetHotKey(button, null); Assert.Empty(tl.KeyBindings); + } + } + + [Fact] + public void HotKeyManager_Release_Reference_When_Control_Detached() + { + using (AvaloniaLocator.EnterScope()) + { + var styler = new Mock(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); + + WeakReference reference = null; + + var tl = new Window(); + + new Action(() => + { + var button = new Button(); + reference = new WeakReference(button, true); + tl.Content = button; + tl.Template = CreateWindowTemplate(); + tl.ApplyTemplate(); + tl.Presenter.ApplyTemplate(); + HotKeyManager.SetHotKey(button, gesture1); + + // Detach the button from the logical tree, so there is no reference to it + tl.Content = null; + tl.ApplyTemplate(); + })(); + + // The button should be collected since it's detached from the listbox + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.Null(reference?.Target); + } + } + + [Fact] + public void HotKeyManager_Release_Reference_When_Control_In_Item_Template_Detached() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var styler = new Mock(); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new WindowingPlatformMock()) + .Bind().ToConstant(styler.Object); + + var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control); + + var weakReferences = new List(); + var tl = new Window { SizeToContent = SizeToContent.WidthAndHeight, IsVisible = true }; + var lm = tl.LayoutManager; + + var keyGestures = new AvaloniaList { gesture1 }; + var listBox = new ListBox + { + Width = 100, + Height = 100, + VirtualizationMode = ItemVirtualizationMode.None, + // Create a button with binding to the KeyGesture in the template and add it to references list + ItemTemplate = new FuncDataTemplate(typeof(KeyGesture), (o, scope) => + { + var keyGesture = o as KeyGesture; + var button = new Button + { + DataContext = keyGesture, [!Button.HotKeyProperty] = new Binding("") + }; + weakReferences.Add(new WeakReference(button, true)); + return button; + }) + }; + // Add the listbox and render it + tl.Content = listBox; + lm.ExecuteInitialLayoutPass(); + listBox.Items = keyGestures; + lm.ExecuteLayoutPass(); + + // Let the button detach when clearing the source items + keyGestures.Clear(); + lm.ExecuteLayoutPass(); + + // Add it again to double check,and render + keyGestures.Add(gesture1); + lm.ExecuteLayoutPass(); + + keyGestures.Clear(); + lm.ExecuteLayoutPass(); + + // The button should be collected since it's detached from the listbox + GC.Collect(); + GC.WaitForPendingFinalizers(); + + + Assert.True(weakReferences.Count > 0); + foreach (var weakReference in weakReferences) + { + Assert.Null(weakReference.Target); + } } } + private FuncControlTemplate CreateWindowTemplate() { return new FuncControlTemplate((parent, scope) => From d58d80d11b8c8e7d038899e99c62e75da59fbc2c Mon Sep 17 00:00:00 2001 From: Adir Date: Fri, 9 Oct 2020 01:34:14 +0300 Subject: [PATCH 008/327] Added another GC collect to the HotKeyManagerTests.cs memory leak tests Renamed tests --- .../Utils/HotKeyManagerTests.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 6425c5c8f4..2e23de366e 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -59,7 +59,7 @@ namespace Avalonia.Controls.UnitTests.Utils } [Fact] - public void HotKeyManager_Release_Reference_When_Control_Detached() + public void HotKeyManager_Should_Release_Reference_When_Control_Detached() { using (AvaloniaLocator.EnterScope()) { @@ -94,13 +94,15 @@ namespace Avalonia.Controls.UnitTests.Utils // The button should be collected since it's detached from the listbox GC.Collect(); GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); Assert.Null(reference?.Target); } } [Fact] - public void HotKeyManager_Release_Reference_When_Control_In_Item_Template_Detached() + public void HotKeyManager_Should_Release_Reference_When_Control_In_Item_Template_Detached() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -154,7 +156,8 @@ namespace Avalonia.Controls.UnitTests.Utils // The button should be collected since it's detached from the listbox GC.Collect(); GC.WaitForPendingFinalizers(); - + GC.Collect(); + GC.WaitForPendingFinalizers(); Assert.True(weakReferences.Count > 0); foreach (var weakReference in weakReferences) From fda517d13e7371c4577974dbc4ea3d6bc5d78201 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2020 14:24:52 +0200 Subject: [PATCH 009/327] Make filename match contained class. --- src/Avalonia.Input/{Cursors.cs => Cursor.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Input/{Cursors.cs => Cursor.cs} (100%) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursor.cs similarity index 100% rename from src/Avalonia.Input/Cursors.cs rename to src/Avalonia.Input/Cursor.cs From 3124152913cf91658836e7d9b4faef246a573178 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2020 16:37:27 +0200 Subject: [PATCH 010/327] Enable NRT for cursor API. --- src/Avalonia.Input/Cursor.cs | 2 ++ src/Avalonia.Input/Platform/IStandardCursorFactory.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 920b598eac..0c858aacdd 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -1,6 +1,8 @@ using System; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Input { /* diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs index 51845cf03e..ede19e91b4 100644 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs @@ -1,5 +1,7 @@ using Avalonia.Input; +#nullable enable + namespace Avalonia.Platform { public interface IStandardCursorFactory From 03a18f56bbc6c70542db99e0056d94b360a78d91 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 17 Oct 2020 16:38:24 +0200 Subject: [PATCH 011/327] Initial support for custom cursors on Win32. --- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 1 + .../HeadlessPlatformStubs.cs | 6 +- src/Avalonia.Input/Cursor.cs | 20 ++--- .../Platform/IStandardCursorFactory.cs | 1 + src/Avalonia.Native/Cursor.cs | 5 ++ src/Avalonia.X11/X11CursorFactory.cs | 5 ++ src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 5 ++ src/Windows/Avalonia.Win32/CursorFactory.cs | 80 +++++++++++++++++++ .../Interop/UnmanagedMethods.cs | 13 +++ .../CursorFactoryMock.cs | 5 ++ 10 files changed, 130 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index f377b1bcd1..a400102877 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -195,6 +195,7 @@ namespace Avalonia.DesignerSupport.Remote class CursorFactoryStub : IStandardCursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new PlatformHandle(IntPtr.Zero, "STUB"); } class IconLoaderStub : IPlatformIconLoader diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 4c0e2982f4..43e6731eee 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -54,11 +54,15 @@ namespace Avalonia.Headless class HeadlessCursorFactoryStub : IStandardCursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) { return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new PlatformHandle(IntPtr.Zero, "STUB"); + } } class HeadlessPlatformSettingsStub : IPlatformSettings diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 0c858aacdd..21c2b1a326 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.Imaging; using Avalonia.Platform; #nullable enable @@ -58,7 +59,12 @@ namespace Avalonia.Input } public Cursor(StandardCursorType cursorType) - : this(GetCursor(cursorType)) + : this(GetCursorFactory().GetCursor(cursorType)) + { + } + + public Cursor(IBitmap cursor, PixelPoint hotSpot) + : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot)) { } @@ -71,16 +77,10 @@ namespace Avalonia.Input throw new ArgumentException($"Unrecognized cursor type '{s}'."); } - private static IPlatformHandle GetCursor(StandardCursorType type) + private static IStandardCursorFactory GetCursorFactory() { - var platform = AvaloniaLocator.Current.GetService(); - - if (platform == null) - { - throw new Exception("Could not create Cursor: IStandardCursorFactory not registered."); - } - - return platform.GetCursor(type); + return AvaloniaLocator.Current.GetService() ?? + throw new Exception("Could not create Cursor: ICursorFactory not registered."); } } } diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs index ede19e91b4..12c3ec7b74 100644 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ b/src/Avalonia.Input/Platform/IStandardCursorFactory.cs @@ -7,5 +7,6 @@ namespace Avalonia.Platform public interface IStandardCursorFactory { IPlatformHandle GetCursor(StandardCursorType cursorType); + IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); } } diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index 3c65367c12..fffff51bce 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -38,5 +38,10 @@ namespace Avalonia.Native var cursor = _native.GetCursor((AvnStandardCursorType)cursorType); return new AvaloniaNativeCursor( cursor ); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 1c995475ae..a82fe76cd5 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -67,6 +67,11 @@ namespace Avalonia.X11 return new PlatformHandle(handle, "XCURSOR"); } + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + throw new NotImplementedException(); + } + private static IntPtr GetNullCursor(IntPtr display) { XColor color = new XColor(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index 7a257da0dd..8763f18cc0 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -10,6 +10,11 @@ namespace Avalonia.LinuxFramebuffer { return new PlatformHandle(IntPtr.Zero, null); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new PlatformHandle(IntPtr.Zero, null); + } } internal class PlatformSettings : IPlatformSettings { diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 8c40eaa7b8..844f230a2e 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -1,8 +1,15 @@ using System; using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; using Avalonia.Input; +using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Win32.Interop; +using SdBitmap = System.Drawing.Bitmap; +using SdPixelFormat = System.Drawing.Imaging.PixelFormat; namespace Avalonia.Win32 { @@ -87,5 +94,78 @@ namespace Avalonia.Win32 return rv; } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using var source = LoadSystemDrawingBitmap(cursor); + using var mask = AlphaToMask(source); + + var info = new UnmanagedMethods.ICONINFO + { + IsIcon = false, + xHotspot = hotSpot.X, + yHotspot = hotSpot.Y, + MaskBitmap = mask.GetHbitmap(), + ColorBitmap = source.GetHbitmap(), + }; + + return new PlatformHandle( + UnmanagedMethods.CreateIconIndirect(ref info), + PlatformConstants.CursorHandleType); + } + + private SdBitmap LoadSystemDrawingBitmap(IBitmapImpl bitmap) + { + using var memoryStream = new MemoryStream(); + bitmap.Save(memoryStream); + return new SdBitmap(memoryStream); + } + + private unsafe SdBitmap AlphaToMask(SdBitmap source) + { + var dest = new SdBitmap(source.Width, source.Height, SdPixelFormat.Format1bppIndexed); + + if (source.PixelFormat != SdPixelFormat.Format32bppArgb && + source.PixelFormat != SdPixelFormat.Format32bppPArgb) + { + return dest; + } + + var sourceData = source.LockBits( + new Rectangle(default, source.Size), + ImageLockMode.ReadOnly, + SdPixelFormat.Format32bppArgb); + var destData = dest.LockBits( + new Rectangle(default, source.Size), + ImageLockMode.ReadOnly, + SdPixelFormat.Format1bppIndexed); + + try + { + var pSource = (byte*)sourceData.Scan0.ToPointer(); + var pDest = (byte*)destData.Scan0.ToPointer(); + + for (var y = 0; y < dest.Height; ++y) + { + for (var x = 0; x < dest.Width; ++x) + { + if (pSource[x * 4] == 0) + { + pDest[x / 8] |= (byte)(1 << (x % 8)); + } + } + + pSource += sourceData.Stride; + pDest += destData.Stride; + } + + return dest; + } + finally + { + source.UnlockBits(sourceData); + dest.UnlockBits(destData); + } + } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5feb6c9e46..c18a5c07c5 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1031,6 +1031,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName); + [DllImport("user32.dll")] + public static extern IntPtr CreateIconIndirect([In] ref ICONINFO iconInfo); + [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); @@ -1743,6 +1746,16 @@ namespace Avalonia.Win32.Interop public int CyContact; } + [StructLayout(LayoutKind.Sequential)] + public struct ICONINFO + { + public bool IsIcon; + public int xHotspot; + public int yHotspot; + public IntPtr MaskBitmap; + public IntPtr ColorBitmap; + }; + [Flags] public enum TouchInputFlags { diff --git a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs index da02cccdc5..66c6033fb4 100644 --- a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs +++ b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs @@ -10,5 +10,10 @@ namespace Avalonia.Controls.UnitTests { return new PlatformHandle(IntPtr.Zero, cursorType.ToString()); } + + public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + return new PlatformHandle(IntPtr.Zero, "Custom"); + } } } From 193cdb3324d82a3dc7207a5bf980fa45bd9b639e Mon Sep 17 00:00:00 2001 From: grokys Date: Mon, 19 Oct 2020 13:44:54 +0200 Subject: [PATCH 012/327] Implement custom cursors on X11. --- src/Avalonia.X11/X11CursorFactory.cs | 59 +++++++++++++++++++++++++++- src/Avalonia.X11/X11Structs.cs | 2 +- src/Avalonia.X11/XLib.cs | 7 ++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index a82fe76cd5..176878ed5f 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Platform; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia.X11 { @@ -67,9 +72,9 @@ namespace Avalonia.X11 return new PlatformHandle(handle, "XCURSOR"); } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public unsafe IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { - throw new NotImplementedException(); + return new XImageCursor(_display, cursor, hotSpot); } private static IntPtr GetNullCursor(IntPtr display) @@ -79,5 +84,55 @@ namespace Avalonia.X11 IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, NullCursorData, 1, 1); return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } + + private unsafe class XImageCursor : IFramebufferPlatformSurface, IPlatformHandle, IDisposable + { + private readonly PixelSize _pixelSize; + private readonly IUnmanagedBlob _blob; + + public XImageCursor(IntPtr display, IBitmapImpl bitmap, PixelPoint hotSpot) + { + var size = Marshal.SizeOf() + + (bitmap.PixelSize.Width * bitmap.PixelSize.Height * 4); + + _pixelSize = bitmap.PixelSize; + _blob = AvaloniaLocator.Current.GetService().AllocBlob(size); + + var image = (XcursorImage*)_blob.Address; + image->version = 1; + image->size = Marshal.SizeOf(); + image->width = bitmap.PixelSize.Width; + image->height = bitmap.PixelSize.Height; + image->xhot = hotSpot.X; + image->yhot = hotSpot.Y; + image->pixels = (IntPtr)(image + 1); + + using (var renderTarget = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[] { this })) + using (var ctx = renderTarget.CreateDrawingContext(null)) + { + var r = new Rect(_pixelSize.ToSize(1)); + ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); + } + + Handle = XLib.XcursorImageLoadCursor(display, _blob.Address); + } + + public IntPtr Handle { get; } + public string HandleDescriptor => "XCURSOR"; + + public void Dispose() + { + XLib.XcursorImageDestroy(Handle); + _blob.Dispose(); + } + + public ILockedFramebuffer Lock() + { + return new LockedFramebuffer( + _blob.Address + Marshal.SizeOf(), + _pixelSize, _pixelSize.Width * 4, + new Vector(96, 96), PixelFormat.Bgra8888, null); + } + } } } diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 0ae5c16aef..7ee764e8a3 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1684,7 +1684,7 @@ namespace Avalonia.X11 { [StructLayout (LayoutKind.Sequential)] internal struct XcursorImage { - private int version; + public int version; public int size; /* nominal size for matching */ public int width; /* actual width */ public int height; /* actual height */ diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 85aa4862b7..f5da691caa 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -19,6 +19,7 @@ namespace Avalonia.X11 const string libX11Randr = "libXrandr.so.2"; const string libX11Ext = "libXext.so.6"; const string libXInput = "libXi.so.6"; + const string libXCursor = "libXcursor.so.1"; [DllImport(libX11)] public static extern IntPtr XOpenDisplay(IntPtr display); @@ -512,6 +513,12 @@ namespace Avalonia.X11 [DllImport(libXInput)] public static extern void XIFreeDeviceInfo(XIDeviceInfo* info); + [DllImport(libXCursor)] + public static extern IntPtr XcursorImageLoadCursor(IntPtr display, IntPtr image); + + [DllImport(libXCursor)] + public static extern IntPtr XcursorImageDestroy(IntPtr image); + public static void XISetMask(ref int mask, XiEventType ev) { mask |= (1 << (int)ev); From a832d639155ea05b84d35fa17a48c32b1e0366b1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 13:46:47 +0200 Subject: [PATCH 013/327] IStandardCursorFactory -> ICursorFactory. --- .../Remote/PreviewerWindowingPlatform.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 2 +- src/Avalonia.Headless/HeadlessPlatformStubs.cs | 2 +- src/Avalonia.Input/Cursor.cs | 4 ++-- .../{IStandardCursorFactory.cs => ICursorFactory.cs} | 2 +- src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- src/Avalonia.Native/Cursor.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 4 ++-- src/Avalonia.X11/X11CursorFactory.cs | 2 +- src/Avalonia.X11/X11Platform.cs | 2 +- .../Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 2 +- src/Windows/Avalonia.Win32/CursorFactory.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- .../CalendarDatePickerTests.cs | 2 +- tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs | 2 +- tests/Avalonia.Controls.UnitTests/DatePickerTests.cs | 2 +- tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs | 4 ++-- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 4 ++-- .../TextBoxTests_DataValidation.cs | 2 +- tests/Avalonia.Controls.UnitTests/TimePickerTests.cs | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 8 ++++---- tests/Avalonia.UnitTests/UnitTestApplication.cs | 2 +- 24 files changed, 31 insertions(+), 31 deletions(-) rename src/Avalonia.Input/Platform/{IStandardCursorFactory.cs => ICursorFactory.cs} (83%) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index fe4c580bbb..67b832318a 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -47,7 +47,7 @@ namespace Avalonia.DesignerSupport.Remote var threading = new InternalPlatformThreadingInterface(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(Keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(threading) diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index a400102877..df1e5d6e98 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -192,7 +192,7 @@ namespace Avalonia.DesignerSupport.Remote public Task GetDataAsync(string format) => Task.FromResult((object)null); } - class CursorFactoryStub : IStandardCursorFactory + class CursorFactoryStub : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new PlatformHandle(IntPtr.Zero, "STUB"); diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 1f750a0309..fca2a1336f 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -58,7 +58,7 @@ namespace Avalonia.Headless AvaloniaLocator.CurrentMutable .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) .Bind().ToSingleton() - .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(new HeadlessPlatformSettingsStub()) .Bind().ToSingleton() .Bind().ToSingleton() diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 43e6731eee..efad5133b9 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -52,7 +52,7 @@ namespace Avalonia.Headless } } - class HeadlessCursorFactoryStub : IStandardCursorFactory + class HeadlessCursorFactoryStub : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) { diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 21c2b1a326..f018041a94 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -77,9 +77,9 @@ namespace Avalonia.Input throw new ArgumentException($"Unrecognized cursor type '{s}'."); } - private static IStandardCursorFactory GetCursorFactory() + private static ICursorFactory GetCursorFactory() { - return AvaloniaLocator.Current.GetService() ?? + return AvaloniaLocator.Current.GetService() ?? throw new Exception("Could not create Cursor: ICursorFactory not registered."); } } diff --git a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs b/src/Avalonia.Input/Platform/ICursorFactory.cs similarity index 83% rename from src/Avalonia.Input/Platform/IStandardCursorFactory.cs rename to src/Avalonia.Input/Platform/ICursorFactory.cs index 12c3ec7b74..e531015d40 100644 --- a/src/Avalonia.Input/Platform/IStandardCursorFactory.cs +++ b/src/Avalonia.Input/Platform/ICursorFactory.cs @@ -4,7 +4,7 @@ using Avalonia.Input; namespace Avalonia.Platform { - public interface IStandardCursorFactory + public interface ICursorFactory { IPlatformHandle GetCursor(StandardCursorType cursorType); IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index e8b2f065c7..c56a8404fa 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -99,7 +99,7 @@ namespace Avalonia.Native AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) - .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) + .Bind().ToConstant(new CursorFactory(_factory.CreateCursorFactory())) .Bind().ToSingleton() .Bind().ToConstant(KeyboardDevice) .Bind().ToConstant(this) diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index fffff51bce..ebee4baafe 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -24,7 +24,7 @@ namespace Avalonia.Native } } - class CursorFactory : IStandardCursorFactory + class CursorFactory : ICursorFactory { IAvnCursorFactory _native; diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 5d35c773d7..e4643bf675 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -53,7 +53,7 @@ namespace Avalonia.Native private bool _gpu = false; private readonly MouseDevice _mouse; private readonly IKeyboardDevice _keyboard; - private readonly IStandardCursorFactory _cursorFactory; + private readonly ICursorFactory _cursorFactory; private Size _savedLogicalSize; private Size _lastRenderedLogicalSize; private double _savedScaling; @@ -68,7 +68,7 @@ namespace Avalonia.Native _keyboard = AvaloniaLocator.Current.GetService(); _mouse = new MouseDevice(); - _cursorFactory = AvaloniaLocator.Current.GetService(); + _cursorFactory = AvaloniaLocator.Current.GetService(); } protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 176878ed5f..7f08407239 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -11,7 +11,7 @@ using Avalonia.Utilities; namespace Avalonia.X11 { - class X11CursorFactory : IStandardCursorFactory + class X11CursorFactory : ICursorFactory { private static readonly byte[] NullCursorData = new byte[] { 0 }; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index c6db146f7b..ffd7c75608 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -52,7 +52,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control)) .Bind().ToFunc(() => KeyboardDevice) - .Bind().ToConstant(new X11CursorFactory(Display)) + .Bind().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 8801f71f9a..340d10517d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -38,7 +38,7 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new RenderLoop()) - .Bind().ToTransient() + .Bind().ToTransient() .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() .Bind().ToConstant(new RenderLoop()) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index 8763f18cc0..def5f6b817 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer { - internal class CursorFactoryStub : IStandardCursorFactory + internal class CursorFactoryStub : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) { diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 844f230a2e..aca5f42771 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -13,7 +13,7 @@ using SdPixelFormat = System.Drawing.Imaging.PixelFormat; namespace Avalonia.Win32 { - internal class CursorFactory : IStandardCursorFactory + internal class CursorFactory : ICursorFactory { public static CursorFactory Instance { get; } = new CursorFactory(); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 61c1a4f45e..ebc69072fc 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -90,7 +90,7 @@ namespace Avalonia.Win32 Options = options; AvaloniaLocator.CurrentMutable .Bind().ToSingleton() - .Bind().ToConstant(CursorFactory.Instance) + .Bind().ToConstant(CursorFactory.Instance) .Bind().ToConstant(WindowsKeyboardDevice.Instance) .Bind().ToConstant(s_instance) .Bind().ToConstant(s_instance) diff --git a/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs index f41a3e7581..d77c7b87fa 100644 --- a/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs @@ -74,7 +74,7 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private CalendarDatePicker CreateControl() { diff --git a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs index 66c6033fb4..0c5a395ba4 100644 --- a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs +++ b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.Controls.UnitTests { - public class CursorFactoryMock : IStandardCursorFactory + public class CursorFactoryMock : ICursorFactory { public IPlatformHandle GetCursor(StandardCursorType cursorType) { diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 7bcb120850..3105263290 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -204,7 +204,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl()); private IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index f2b6b0db4b..72ccead783 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -11,8 +11,8 @@ namespace Avalonia.Controls.UnitTests { public GridSplitterTests() { - var cursorFactoryImpl = new Mock(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); + var cursorFactoryImpl = new Mock(); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(cursorFactoryImpl.Object); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index fe25fa7346..6ac7799828 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -698,10 +698,10 @@ namespace Avalonia.Controls.UnitTests keyboardDevice: () => new KeyboardDevice(), keyboardNavigation: new KeyboardNavigationHandler(), inputManager: new InputManager(), - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 78b6bf01ec..570b9ee4ea 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls.UnitTests } private static TestServices Services => TestServices.MockThreadingInterface.With( - standardCursorFactory: Mock.Of()); + standardCursorFactory: Mock.Of()); private IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index 682f0eaadb..c39a963dae 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -100,7 +100,7 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl()); private IControlTemplate CreateTemplate() diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 012cab23dc..8d27562146 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -23,7 +23,7 @@ namespace Avalonia.UnitTests assetLoader: new AssetLoader(), platform: new AppBuilder().RuntimePlatform, renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), + standardCursorFactory: Mock.Of(), styler: new Styler(), theme: () => CreateDefaultTheme(), threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true), @@ -70,7 +70,7 @@ namespace Avalonia.UnitTests IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, IScheduler scheduler = null, - IStandardCursorFactory standardCursorFactory = null, + ICursorFactory standardCursorFactory = null, IStyler styler = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, @@ -111,7 +111,7 @@ namespace Avalonia.UnitTests public IFontManagerImpl FontManagerImpl { get; } public ITextShaperImpl TextShaperImpl { get; } public IScheduler Scheduler { get; } - public IStandardCursorFactory StandardCursorFactory { get; } + public ICursorFactory StandardCursorFactory { get; } public IStyler Styler { get; } public Func Theme { get; } public IPlatformThreadingInterface ThreadingInterface { get; } @@ -130,7 +130,7 @@ namespace Avalonia.UnitTests IPlatformRenderInterface renderInterface = null, IRenderTimer renderLoop = null, IScheduler scheduler = null, - IStandardCursorFactory standardCursorFactory = null, + ICursorFactory standardCursorFactory = null, IStyler styler = null, Func theme = null, IPlatformThreadingInterface threadingInterface = null, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index e4a65f105d..6fc82088e9 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -62,7 +62,7 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.TextShaperImpl) .Bind().ToConstant(Services.ThreadingInterface) .Bind().ToConstant(Services.Scheduler) - .Bind().ToConstant(Services.StandardCursorFactory) + .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.Styler) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToSingleton(); From ca408e55b5091449158cfae7536960464389f6fd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 14:32:28 +0200 Subject: [PATCH 014/327] Added ICursorImpl. --- .../Offscreen/OffscreenTopLevelImpl.cs | 2 +- .../Platform/ITopLevelImpl.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 11 ++-- .../HeadlessPlatformStubs.cs | 10 ++-- src/Avalonia.Headless/HeadlessWindowImpl.cs | 2 +- src/Avalonia.Input/Cursor.cs | 6 +-- src/Avalonia.Input/Platform/ICursorFactory.cs | 4 +- src/Avalonia.Input/Platform/ICursorImpl.cs | 14 +++++ src/Avalonia.Native/Cursor.cs | 6 +-- src/Avalonia.Native/WindowImplBase.cs | 2 +- src/Avalonia.X11/X11CursorFactory.cs | 19 ++++--- src/Avalonia.X11/X11Window.cs | 8 ++- .../FramebufferToplevelImpl.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 10 ++-- .../Wpf/WpfTopLevelImpl.cs | 6 +-- src/Windows/Avalonia.Win32/CursorFactory.cs | 52 ++++++++++++------- .../Interop/UnmanagedMethods.cs | 3 ++ src/Windows/Avalonia.Win32/WindowImpl.cs | 15 ++++-- .../CursorFactoryMock.cs | 16 ++++-- 20 files changed, 121 insertions(+), 71 deletions(-) create mode 100644 src/Avalonia.Input/Platform/ICursorImpl.cs diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 522103c7bd..ca0e9d48b8 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public virtual PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, 1); - public virtual void SetCursor(IPlatformHandle cursor) + public virtual void SetCursor(ICursorImpl cursor) { } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 7514f214aa..09f38042a1 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -98,7 +98,7 @@ namespace Avalonia.Platform /// Sets the cursor associated with the toplevel. /// /// The cursor. Use null for default cursor - void SetCursor(IPlatformHandle cursor); + void SetCursor(ICursorImpl cursor); /// /// Gets or sets a method called when the underlying implementation is destroyed. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 3d24f60463..ffccdeb3e2 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -162,7 +162,7 @@ namespace Avalonia.Controls this.GetObservable(PointerOverElementProperty) .Select( x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) - .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); + .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformImpl)); if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index df1e5d6e98..751c5c08f2 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -73,7 +73,7 @@ namespace Avalonia.DesignerSupport.Remote public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } @@ -194,8 +194,13 @@ namespace Avalonia.DesignerSupport.Remote class CursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new PlatformHandle(IntPtr.Zero, "STUB"); + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); + + private class CursorStub : ICursorImpl + { + public void Dispose() { } + } } class IconLoaderStub : IPlatformIconLoader diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index efad5133b9..ce4c31e27e 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -54,14 +54,12 @@ namespace Avalonia.Headless class HeadlessCursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) - { - return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); - } + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + private class CursorStub : ICursorImpl { - return new PlatformHandle(IntPtr.Zero, "STUB"); + public void Dispose() { } } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 8f4fa5e304..2c52438743 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.Headless public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index f018041a94..052ef5e9a0 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -53,9 +53,9 @@ namespace Avalonia.Input { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); - internal Cursor(IPlatformHandle platformCursor) + internal Cursor(ICursorImpl platformImpl) { - PlatformCursor = platformCursor; + PlatformImpl = platformImpl; } public Cursor(StandardCursorType cursorType) @@ -68,7 +68,7 @@ namespace Avalonia.Input { } - public IPlatformHandle PlatformCursor { get; } + public ICursorImpl PlatformImpl { get; } public static Cursor Parse(string s) { diff --git a/src/Avalonia.Input/Platform/ICursorFactory.cs b/src/Avalonia.Input/Platform/ICursorFactory.cs index e531015d40..fff1f92d53 100644 --- a/src/Avalonia.Input/Platform/ICursorFactory.cs +++ b/src/Avalonia.Input/Platform/ICursorFactory.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform { public interface ICursorFactory { - IPlatformHandle GetCursor(StandardCursorType cursorType); - IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); + ICursorImpl GetCursor(StandardCursorType cursorType); + ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot); } } diff --git a/src/Avalonia.Input/Platform/ICursorImpl.cs b/src/Avalonia.Input/Platform/ICursorImpl.cs new file mode 100644 index 0000000000..14235869f7 --- /dev/null +++ b/src/Avalonia.Input/Platform/ICursorImpl.cs @@ -0,0 +1,14 @@ +using System; +using Avalonia.Input; + +#nullable enable + +namespace Avalonia.Platform +{ + /// + /// Represents a platform implementation of a . + /// + public interface ICursorImpl : IDisposable + { + } +} diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs index ebee4baafe..2cb085bc7e 100644 --- a/src/Avalonia.Native/Cursor.cs +++ b/src/Avalonia.Native/Cursor.cs @@ -5,7 +5,7 @@ using Avalonia.Native.Interop; namespace Avalonia.Native { - class AvaloniaNativeCursor : IPlatformHandle, IDisposable + class AvaloniaNativeCursor : ICursorImpl, IDisposable { public IAvnCursor Cursor { get; private set; } public IntPtr Handle => IntPtr.Zero; @@ -33,13 +33,13 @@ namespace Avalonia.Native _native = native; } - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { var cursor = _native.GetCursor((AvnStandardCursorType)cursorType); return new AvaloniaNativeCursor( cursor ); } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index e4643bf675..ad2b7411eb 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -389,7 +389,7 @@ namespace Avalonia.Native public Action Deactivated { get; set; } public Action Activated { get; set; } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { if (_native == null) { diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 7f08407239..f95d4320fe 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -56,7 +56,7 @@ namespace Avalonia.X11 .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { IntPtr handle; if (cursorType == StandardCursorType.None) @@ -69,10 +69,10 @@ namespace Avalonia.X11 ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; } - return new PlatformHandle(handle, "XCURSOR"); + return new CursorImpl(handle); } - public unsafe IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { return new XImageCursor(_display, cursor, hotSpot); } @@ -85,7 +85,7 @@ namespace Avalonia.X11 return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } - private unsafe class XImageCursor : IFramebufferPlatformSurface, IPlatformHandle, IDisposable + private unsafe class XImageCursor : CursorImpl, IFramebufferPlatformSurface, IPlatformHandle { private readonly PixelSize _pixelSize; private readonly IUnmanagedBlob _blob; @@ -117,10 +117,9 @@ namespace Avalonia.X11 Handle = XLib.XcursorImageLoadCursor(display, _blob.Address); } - public IntPtr Handle { get; } public string HandleDescriptor => "XCURSOR"; - public void Dispose() + public override void Dispose() { XLib.XcursorImageDestroy(Handle); _blob.Dispose(); @@ -135,4 +134,12 @@ namespace Avalonia.X11 } } } + + class CursorImpl : ICursorImpl + { + public CursorImpl() { } + public CursorImpl(IntPtr handle) => Handle = handle; + public IntPtr Handle { get; protected set; } + public virtual void Dispose() { } + } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2cd3b973d8..32c455571f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -878,15 +878,13 @@ namespace Avalonia.X11 UpdateSizeHints(null); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { if (cursor == null) XDefineCursor(_x11.Display, _handle, _x11.DefaultCursor); - else + else if (cursor is CursorImpl impl) { - if (cursor.HandleDescriptor != "XCURSOR") - throw new ArgumentException("Expected XCURSOR handle type"); - XDefineCursor(_x11.Display, _handle, cursor.Handle); + XDefineCursor(_x11.Display, _handle, impl.Handle); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 0a101eec7a..4bbb58e53e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -57,7 +57,7 @@ namespace Avalonia.LinuxFramebuffer public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1); - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs index def5f6b817..642be28c69 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -6,14 +6,12 @@ namespace Avalonia.LinuxFramebuffer { internal class CursorFactoryStub : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) - { - return new PlatformHandle(IntPtr.Zero, null); - } + public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + private class CursorStub : ICursorImpl { - return new PlatformHandle(IntPtr.Zero, null); + public void Dispose() { } } } internal class PlatformSettings : IPlatformSettings diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 3467a33d16..3bb29f4e23 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -225,12 +225,12 @@ namespace Avalonia.Win32.Interop.Wpf protected override void OnTextInput(TextCompositionEventArgs e) => _ttl.Input?.Invoke(new RawTextInputEventArgs(_keyboard, (uint) e.Timestamp, _inputRoot, e.Text)); - void ITopLevelImpl.SetCursor(IPlatformHandle cursor) + void ITopLevelImpl.SetCursor(ICursorImpl cursor) { if (cursor == null) Cursor = Cursors.Arrow; - else if (cursor.HandleDescriptor == "HCURSOR") - Cursor = CursorShim.FromHCursor(cursor.Handle); + else if (cursor is IPlatformHandle handle) + Cursor = CursorShim.FromHCursor(handle.Handle); } Action ITopLevelImpl.Input { get; set; } //TODO diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index aca5f42771..10878ba7b5 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; -using System.Runtime.InteropServices; using Avalonia.Input; -using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Win32.Interop; using SdBitmap = System.Drawing.Bitmap; @@ -36,8 +34,7 @@ namespace Avalonia.Win32 IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id)); if (cursor != IntPtr.Zero) { - PlatformHandle phCursor = new PlatformHandle(cursor, PlatformConstants.CursorHandleType); - Cache.Add(cursorType, phCursor); + Cache.Add(cursorType, new CursorImpl(cursor, false)); } } } @@ -77,25 +74,23 @@ namespace Avalonia.Win32 {StandardCursorType.DragLink, 32516}, }; - private static readonly Dictionary Cache = - new Dictionary(); + private static readonly Dictionary Cache = + new Dictionary(); - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { - IPlatformHandle rv; - if (!Cache.TryGetValue(cursorType, out rv)) + if (!Cache.TryGetValue(cursorType, out var rv)) { - Cache[cursorType] = - rv = - new PlatformHandle( - UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])), - PlatformConstants.CursorHandleType); + rv = new CursorImpl( + UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])), + false); + Cache.Add(cursorType, rv); } return rv; } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { using var source = LoadSystemDrawingBitmap(cursor); using var mask = AlphaToMask(source); @@ -109,9 +104,7 @@ namespace Avalonia.Win32 ColorBitmap = source.GetHbitmap(), }; - return new PlatformHandle( - UnmanagedMethods.CreateIconIndirect(ref info), - PlatformConstants.CursorHandleType); + return new CursorImpl(UnmanagedMethods.CreateIconIndirect(ref info), true); } private SdBitmap LoadSystemDrawingBitmap(IBitmapImpl bitmap) @@ -168,4 +161,27 @@ namespace Avalonia.Win32 } } } + + internal class CursorImpl : ICursorImpl, IPlatformHandle + { + private readonly bool _isCustom; + + public CursorImpl(IntPtr handle, bool isCustom) + { + Handle = handle; + _isCustom = isCustom; + } + + public IntPtr Handle { get; private set; } + public string HandleDescriptor => PlatformConstants.CursorHandleType; + + public void Dispose() + { + if (_isCustom && Handle != IntPtr.Zero) + { + UnmanagedMethods.DestroyIcon(Handle); + Handle = IntPtr.Zero; + } + } + } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index c18a5c07c5..3b11aecc2d 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1034,6 +1034,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr CreateIconIndirect([In] ref ICONINFO iconInfo); + [DllImport("user32.dll")] + public static extern bool DestroyIcon(IntPtr hIcon); + [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 7079a0120c..755570bff1 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -557,14 +557,19 @@ namespace Avalonia.Win32 SetWindowText(_hwnd, title); } - public void SetCursor(IPlatformHandle cursor) + public void SetCursor(ICursorImpl cursor) { - var hCursor = cursor?.Handle ?? DefaultCursor; - SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); + var impl = cursor as CursorImpl; - if (_owner.IsPointerOver) + if (cursor is null || impl is object) { - UnmanagedMethods.SetCursor(hCursor); + var hCursor = impl?.Handle ?? DefaultCursor; + SetClassLong(_hwnd, ClassLongIndex.GCLP_HCURSOR, hCursor); + + if (_owner.IsPointerOver) + { + UnmanagedMethods.SetCursor(hCursor); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs index 0c5a395ba4..ee4264e6b9 100644 --- a/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs +++ b/tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Input; using Avalonia.Platform; @@ -6,14 +5,21 @@ namespace Avalonia.Controls.UnitTests { public class CursorFactoryMock : ICursorFactory { - public IPlatformHandle GetCursor(StandardCursorType cursorType) + public ICursorImpl GetCursor(StandardCursorType cursorType) { - return new PlatformHandle(IntPtr.Zero, cursorType.ToString()); + return new MockCursorImpl(); } - public IPlatformHandle CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) { - return new PlatformHandle(IntPtr.Zero, "Custom"); + return new MockCursorImpl(); + } + + private class MockCursorImpl : ICursorImpl + { + public void Dispose() + { + } } } } From f0d097ed1234a9a45ab42451c52bba8afc4ac37d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 14:34:50 +0200 Subject: [PATCH 015/327] Make Cursor disposable. --- src/Avalonia.Input/Cursor.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 052ef5e9a0..2b99c51472 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -6,13 +6,6 @@ using Avalonia.Platform; namespace Avalonia.Input { - /* - ========================================================================================= - NOTE: Cursors are NOT disposable and are cached in platform implementation. - To support loading custom cursors some measures about that should be taken beforehand - ========================================================================================= - */ - public enum StandardCursorType { Arrow, @@ -49,7 +42,7 @@ namespace Avalonia.Input // SizeNorthEastSouthWest, } - public class Cursor + public class Cursor : IDisposable { public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow); @@ -70,6 +63,8 @@ namespace Avalonia.Input public ICursorImpl PlatformImpl { get; } + public void Dispose() => PlatformImpl.Dispose(); + public static Cursor Parse(string s) { return Enum.TryParse(s, true, out var t) ? From 04f5d3a252f1c029ccc64f16684c07eb8cd6c798 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 20 Oct 2020 14:38:01 +0200 Subject: [PATCH 016/327] Don't allow premultipied alpha for the moment. --- src/Windows/Avalonia.Win32/CursorFactory.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 10878ba7b5..0a0e45b03a 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -118,8 +118,13 @@ namespace Avalonia.Win32 { var dest = new SdBitmap(source.Width, source.Height, SdPixelFormat.Format1bppIndexed); - if (source.PixelFormat != SdPixelFormat.Format32bppArgb && - source.PixelFormat != SdPixelFormat.Format32bppPArgb) + if (source.PixelFormat == SdPixelFormat.Format32bppPArgb) + { + throw new NotSupportedException( + "Images with premultiplied alpha not yet supported as cursor images."); + } + + if (source.PixelFormat != SdPixelFormat.Format32bppArgb) { return dest; } From 98974af13e7e08662c32b4744e6df9f89490d97c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 21 Oct 2020 14:44:24 +0200 Subject: [PATCH 017/327] Added cursor page to control catalog. --- samples/ControlCatalog/Assets/avalonia-32.png | Bin 0 -> 1322 bytes samples/ControlCatalog/MainView.xaml | 4 ++ samples/ControlCatalog/Pages/CursorPage.xaml | 29 ++++++++++++ .../ControlCatalog/Pages/CursorPage.xaml.cs | 20 +++++++++ .../ViewModels/CursorPageViewModel.cs | 42 ++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 samples/ControlCatalog/Assets/avalonia-32.png create mode 100644 samples/ControlCatalog/Pages/CursorPage.xaml create mode 100644 samples/ControlCatalog/Pages/CursorPage.xaml.cs create mode 100644 samples/ControlCatalog/ViewModels/CursorPageViewModel.cs diff --git a/samples/ControlCatalog/Assets/avalonia-32.png b/samples/ControlCatalog/Assets/avalonia-32.png new file mode 100644 index 0000000000000000000000000000000000000000..7b443e7a250846dc898e6467cddeed8c55880766 GIT binary patch literal 1322 zcmV+_1=aeAP)A zL5V<&1`?F`W{`LZcsE5cBpMzF4;mv6L;;~9STsnu#G;m4u_XwFBGRT;C>`6GUgn(T zL)$4c=S({l;{UQ|_FDV=|6Y6Tb=DCggsQrL1;9dJ29OIR^qV_@TA&g*4xAK`-bkN4 z9C5m;mZ)ljMvf*`-Kwf%?}yk>)mK&3GXmmJbg1eZ{m~l{d7`S8-Uo3gep1zxNXaLv zYSjZIAHbigniI+=A>>IwIZ!Z^$o9rgT58+rX!W5AiD_}9PaZ`|X5w)By8#r7NUKeN zsv5v|z%r+mmfzx!BR9Bs_y#St?T$}Rn>32TB~y5G>114PBbTZ98d$;Vp#KA9s55xh1snb zVql0XA`$Lr7fUu3F?C*+li3I4iAdc*oHjU-H`Lr_?|aAS_Vr+?iKSLxRCGuI{vMMb zK0ZUk2-=%=h?IY zi`~EzPRh&oS7NDYn_skkCi9*z44TU>OeecAopT4SaeU`R+nUNljsgG{8T7qa0_gPk zXlwS6nU~go-h*d9ZQcYM`MsF6BA5Jy*|vF4lb7b}cb%5ZFn}oo|JrZ=!;Uw1vwh<( zW-ZO<@uv%dM9f`1WPJjlXysJL`Q~em0&)x>8Nl1o#aATvT{Op4xk1SOI5BuQ<-fC&z>p5V$uYLPByp$*GA{ zp16$P^aoYHvq!3^ID9EAdG)bc&K+_VkT53JX<7~XdN>>AE`KbD+)@+MviN388T+;# zrTOMyiM?Id&f(9>`L^T)rsa?TLqJqD7H9y1I+mWUUfx>%DV|nu*isxDAA`$q)9W*X zY)MfmgjVRHRe3B}Z@b2pAtGJC?odl~Y!ok+tVCScA~7&_dAsp-`bPYsx)%e5ejCso zYRoU1!kTxMgP~zX432}pM*;&tL~atM8JV+eCL4CFC1G@I#3CZ87oiaV_z*bhHmSh z$FX-7ktX^Yl}~^N^O5O{ABXi&)f`p*LRC8-@RyQDRdXYjs;Wt<`dq)5v#Q$iA4n}# g?NZe;Rhb%7 literal 0 HcmV?d00001 diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index bd5beafe29..34ff4b1a54 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -22,6 +22,10 @@ + + + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml new file mode 100644 index 0000000000..a28039ea3f --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml @@ -0,0 +1,29 @@ + + + + Cursor + Defines a cursor (mouse pointer) + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml.cs b/samples/ControlCatalog/Pages/CursorPage.xaml.cs new file mode 100644 index 0000000000..9e9e9ba8b9 --- /dev/null +++ b/samples/ControlCatalog/Pages/CursorPage.xaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class CursorPage : UserControl + { + public CursorPage() + { + this.InitializeComponent(); + DataContext = new CursorPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs new file mode 100644 index 0000000000..a4c643dd04 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class CursorPageViewModel : ReactiveObject + { + public CursorPageViewModel() + { + StandardCursors = Enum.GetValues(typeof(StandardCursorType)) + .Cast() + .Select(x => new StandardCursorModel(x)) + .ToList(); + + var loader = AvaloniaLocator.Current.GetService(); + var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); + var bitmap = new Bitmap(s); + CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); + } + + public IEnumerable StandardCursors { get; } + public Cursor CustomCursor { get; } + + public class StandardCursorModel + { + public StandardCursorModel(StandardCursorType type) + { + Type = type; + Cursor = new Cursor(type); + } + + public StandardCursorType Type { get; } + public Cursor Cursor { get; } + } + } +} From 41c4630bbb2fac9e9247f864d510079a9e60327b Mon Sep 17 00:00:00 2001 From: Adir Date: Fri, 23 Oct 2020 14:08:05 +0300 Subject: [PATCH 018/327] Changed if statements to have braces Moved comments above if --- src/Avalonia.Controls/Button.cs | 8 +++++--- src/Avalonia.Controls/MenuItem.cs | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 87fabf5ed7..9cd7a19e7e 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -208,9 +208,11 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - if (_hotkey != null) - // Control attached again, set Hotkey to create a hotkey manager for this control + if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control + { HotKey = _hotkey; + } + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -221,9 +223,9 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + // This will cause the hotkey manager to dispose the observer and the reference to this control if (HotKey != null) { - // This will cause the hotkey manager to dispose the observer and the reference to this control _hotkey = HotKey; HotKey = null; } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index faf92c97fe..8612f696bc 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -338,9 +338,11 @@ namespace Avalonia.Controls protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - if (_hotkey != null) - // Control attached again, set Hotkey to create a hotkey manager for this control + if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control + { HotKey = _hotkey; + } + base.OnAttachedToLogicalTree(e); if (Command != null) @@ -351,9 +353,9 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { + // This will cause the hotkey manager to dispose the observer and the reference to this control if (HotKey != null) { - // This will cause the hotkey manager to dispose the observer and the reference to this control _hotkey = HotKey; HotKey = null; } From 34b5f07e9656855e20ec42c292aa0b738c3b186a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 13 Nov 2020 12:23:08 +0100 Subject: [PATCH 019/327] Added failing tests for #5027. We shouldn't subscribe to bindings until needed. --- .../Avalonia.Markup.Xaml.UnitTests.csproj | 1 + .../DynamicResourceExtensionTests.cs | 96 +++++++++++++++++++ .../Avalonia.Styling.UnitTests/StyleTests.cs | 82 ++++++++++++++++ 3 files changed, 179 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index ad3592294d..b070765a60 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -3,6 +3,7 @@ netcoreapp3.1;net47 Library true + latest diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 47a73cb360..410f579a13 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -792,6 +793,82 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(0xff506070, brush.Color.ToUint32()); } + [Fact] + public void Resource_In_Non_Matching_Style_Is_Not_Resolved() + { + using var app = StyledWindow(); + + var xaml = @" + + + + + + + + + + + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var border = window.FindControl("border"); + + Assert.Equal("bar", border.Tag); + + var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; + Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + } + + [Fact] + public void Resource_In_Non_Active_Style_Is_Not_Resolved() + { + using var app = StyledWindow(); + + var xaml = @" + + + + + + + + + + + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var border = window.FindControl("border"); + + Assert.Equal("bar", border.Tag); + + var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0]; + Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources); + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( @@ -822,4 +899,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions }; } } + + public class TrackingResourceProvider : IResourceProvider + { + public IResourceHost Owner { get; private set; } + public bool HasResources => true; + public List RequestedResources { get; } = new List(); + + public event EventHandler OwnerChanged; + + public void AddOwner(IResourceHost owner) => Owner = owner; + public void RemoveOwner(IResourceHost owner) => Owner = null; + + public bool TryGetResource(object key, out object value) + { + RequestedResources.Add(key); + value = key; + return true; + } + } } diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index df94887340..61d9f961f3 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Controls; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.UnitTests; using Moq; @@ -217,6 +218,78 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(new[] { "foodefault", "Bar" }, values); } + [Fact] + public void Template_In_Non_Matching_Style_Is_Not_Built() + { + var instantiationCount = 0; + var template = new FuncTemplate(() => + { + ++instantiationCount; + return new Class1(); + }); + + Styles styles = new Styles + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.ChildProperty, template), + }, + }, + + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Class1.ChildProperty, template), + }, + } + }; + + var target = new Class1(); + styles.TryAttach(target, null); + + Assert.NotNull(target.Child); + Assert.Equal(1, instantiationCount); + } + + [Fact] + public void Template_In_Inactive_Style_Is_Not_Built() + { + var instantiationCount = 0; + var template = new FuncTemplate(() => + { + ++instantiationCount; + return new Class1(); + }); + + Styles styles = new Styles + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Class1.ChildProperty, template), + }, + }, + + new Style(x => x.OfType()) + { + Setters = + { + new Setter(Class1.ChildProperty, template), + }, + } + }; + + var target = new Class1(); + styles.TryAttach(target, null); + + Assert.NotNull(target.Child); + Assert.Equal(1, instantiationCount); + } + [Fact] public void Style_Should_Detach_When_Control_Removed_From_Logical_Tree() { @@ -453,12 +526,21 @@ namespace Avalonia.Styling.UnitTests public static readonly StyledProperty FooProperty = AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty ChildProperty = + AvaloniaProperty.Register(nameof(Child)); + public string Foo { get { return GetValue(FooProperty); } set { SetValue(FooProperty, value); } } + public Class1 Child + { + get => GetValue(ChildProperty); + set => SetValue(ChildProperty, value); + } + protected override Size MeasureOverride(Size availableSize) { throw new NotImplementedException(); From 74695977123b4794e62cb188c40b9832bd3c11e4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 16 Nov 2020 08:39:31 +0100 Subject: [PATCH 020/327] Added benchmark for fluent RepeatButton. As that's where #5027 was showing up most. --- .../Avalonia.Benchmarks.csproj | 1 + .../Themes/FluentBenchmark.cs | 74 +++++++++++++++++++ .../Avalonia.UnitTests/UnitTestApplication.cs | 2 +- 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 251894e324..e8e69efdbc 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs new file mode 100644 index 0000000000..3a05502371 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -0,0 +1,74 @@ +using System; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Platform; +using Avalonia.Shared.PlatformSupport; +using Avalonia.Styling; +using Avalonia.UnitTests; +using BenchmarkDotNet.Attributes; +using Moq; + +namespace Avalonia.Benchmarks.Themes +{ + [MemoryDiagnoser] + public class FluentBenchmark + { + private readonly IDisposable _app; + private readonly TestRoot _root; + + public FluentBenchmark() + { + _app = CreateApp(); + _root = new TestRoot(true, null) + { + Renderer = new NullRenderer() + }; + + _root.LayoutManager.ExecuteInitialLayoutPass(); + } + + public void Dispose() + { + _app.Dispose(); + } + + [Benchmark] + public void RepeatButton() + { + var button = new RepeatButton(); + _root.Child = button; + _root.LayoutManager.ExecuteLayoutPass(); + } + + private static IDisposable CreateApp() + { + var services = new TestServices( + assetLoader: new AssetLoader(), + globalClock: new MockGlobalClock(), + platform: new AppBuilder().RuntimePlatform, + renderInterface: new MockPlatformRenderInterface(), + standardCursorFactory: Mock.Of(), + styler: new Styler(), + theme: () => LoadFluentTheme(), + threadingInterface: new NullThreadingPlatform(), + fontManagerImpl: new MockFontManagerImpl(), + textShaperImpl: new MockTextShaperImpl(), + windowingPlatform: new MockWindowingPlatform()); + + return UnitTestApplication.Start(services); + } + + private static Styles LoadFluentTheme() + { + AssetLoader.RegisterResUriParsers(); + return new Styles + { + new StyleInclude(new Uri("avares://Avalonia.Benchmarks")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml") + } + }; + } + } +} diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index e4a65f105d..4e3f1ad28a 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -25,6 +25,7 @@ namespace Avalonia.UnitTests public UnitTestApplication(TestServices services) { _services = services ?? new TestServices(); + AvaloniaLocator.CurrentMutable.BindToSelf(this); RegisterServices(); } @@ -36,7 +37,6 @@ namespace Avalonia.UnitTests { var scope = AvaloniaLocator.EnterScope(); var app = new UnitTestApplication(services); - AvaloniaLocator.CurrentMutable.BindToSelf(app); Dispatcher.UIThread.UpdateServices(); return Disposable.Create(() => { From d8fbd95ef03288f3262f4de425fb1b335f17073e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 12 Nov 2020 11:49:53 +0100 Subject: [PATCH 021/327] Don't subscribe to inner observable if not active. When subscribing to a `PropertySetterBindingInstance`, if the owner style is not active then there's no need to subscribe to the inner observable as this can cause resource lookups etc. Part of fixing #5027. --- .../Styling/PropertySetterBindingInstance.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs index 929f7142bb..a67190c831 100644 --- a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -92,6 +92,7 @@ namespace Avalonia.Styling { if (!_isActive) { + _innerSubscription ??= _binding.Observable.Subscribe(_inner); _isActive = true; PublishNext(); } @@ -102,6 +103,8 @@ namespace Avalonia.Styling if (_isActive) { _isActive = false; + _innerSubscription?.Dispose(); + _innerSubscription = null; PublishNext(); } } @@ -148,7 +151,10 @@ namespace Avalonia.Styling protected override void Subscribed() { - _innerSubscription = _binding.Observable.Subscribe(_inner); + if (_isActive) + { + _innerSubscription = _binding.Observable.Subscribe(_inner); + } } protected override void Unsubscribed() From 72a43174e18d668611d86f3a3210220643f369ce Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 14 Nov 2020 16:50:53 +0100 Subject: [PATCH 022/327] Lazily evaluate