Browse Source

Merge pull request #4887 from AvaloniaUI/feature/custom-cursors

Custom cursors
Dariusz Komosiński 4 years ago
parent
commit
61503e99e0
50 changed files with 500 additions and 112 deletions
  1. 22 0
      native/Avalonia.Native/src/OSX/cursor.mm
  2. BIN
      samples/ControlCatalog/Assets/avalonia-32.png
  3. 4 0
      samples/ControlCatalog/MainView.xaml
  4. 29 0
      samples/ControlCatalog/Pages/CursorPage.xaml
  5. 20 0
      samples/ControlCatalog/Pages/CursorPage.xaml.cs
  6. 44 0
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  7. 6 0
      src/Avalonia.Controls/ApiCompatBaseline.txt
  8. 1 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  9. 1 1
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  10. 1 1
      src/Avalonia.Controls/TopLevel.cs
  11. 1 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  12. 9 3
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  13. 1 1
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  14. 5 3
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  15. 1 1
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  16. 4 0
      src/Avalonia.Input/ApiCompatBaseline.txt
  17. 18 21
      src/Avalonia.Input/Cursor.cs
  18. 12 0
      src/Avalonia.Input/Platform/ICursorFactory.cs
  19. 14 0
      src/Avalonia.Input/Platform/ICursorImpl.cs
  20. 0 9
      src/Avalonia.Input/Platform/IStandardCursorFactory.cs
  21. 1 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  22. 22 3
      src/Avalonia.Native/Cursor.cs
  23. 3 3
      src/Avalonia.Native/WindowImplBase.cs
  24. 1 0
      src/Avalonia.Native/avn.idl
  25. 70 3
      src/Avalonia.X11/X11CursorFactory.cs
  26. 1 1
      src/Avalonia.X11/X11Platform.cs
  27. 1 1
      src/Avalonia.X11/X11Structs.cs
  28. 3 5
      src/Avalonia.X11/X11Window.cs
  29. 7 0
      src/Avalonia.X11/XLib.cs
  30. 1 1
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  31. 1 1
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  32. 6 3
      src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs
  33. 3 3
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  34. 114 13
      src/Windows/Avalonia.Win32/CursorFactory.cs
  35. 16 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  36. 1 1
      src/Windows/Avalonia.Win32/Win32Platform.cs
  37. 10 5
      src/Windows/Avalonia.Win32/WindowImpl.cs
  38. 2 2
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  39. 1 1
      src/iOS/Avalonia.iOS/Platform.cs
  40. 9 3
      src/iOS/Avalonia.iOS/Stubs.cs
  41. 6 3
      tests/Avalonia.Benchmarks/NullCursorFactory.cs
  42. 1 1
      tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs
  43. 15 4
      tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs
  44. 1 1
      tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
  45. 2 2
      tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs
  46. 2 2
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  47. 1 1
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
  48. 1 1
      tests/Avalonia.Controls.UnitTests/TimePickerTests.cs
  49. 4 4
      tests/Avalonia.UnitTests/TestServices.cs
  50. 1 1
      tests/Avalonia.UnitTests/UnitTestApplication.cs

+ 22 - 0
native/Avalonia.Native/src/OSX/cursor.mm

@@ -62,6 +62,28 @@ public:
             
         return S_OK;
     }
+    
+    virtual HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut) override
+    {
+        if(bitmapData == nullptr || retOut == nullptr)
+        {
+            return E_POINTER;
+        }
+        
+        NSData *imageData = [NSData dataWithBytes:bitmapData length:length];
+        NSImage *image = [[NSImage alloc] initWithData:imageData];
+        
+        
+        NSPoint hotSpot;
+        hotSpot.x = hotPixel.Width;
+        hotSpot.y = hotPixel.Height;
+        
+        *retOut = new Cursor([[NSCursor new] initWithImage: image hotSpot: hotSpot]);
+        
+        (*retOut)->AddRef();
+        
+        return S_OK;
+    }
 };
 
 extern IAvnCursorFactory* CreateCursorFactory()

BIN
samples/ControlCatalog/Assets/avalonia-32.png


+ 4 - 0
samples/ControlCatalog/MainView.xaml

@@ -22,6 +22,10 @@
       <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
       <TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
       <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
+      <TabItem Header="Cursor"
+               ScrollViewer.VerticalScrollBarVisibility="Disabled">
+        <pages:CursorPage/>
+      </TabItem>
       <TabItem Header="DataGrid" 
                ScrollViewer.VerticalScrollBarVisibility="Disabled"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled">

+ 29 - 0
samples/ControlCatalog/Pages/CursorPage.xaml

@@ -0,0 +1,29 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.CursorPage">
+  <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*">
+    <StackPanel Grid.ColumnSpan="2" Orientation="Vertical" Spacing="4">
+      <TextBlock Classes="h1">Cursor</TextBlock>
+      <TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock>
+    </StackPanel>
+
+    <ListBox Grid.Row="1" Items="{Binding StandardCursors}" Margin="0 8 8 8">
+      <ListBox.Styles>
+        <Style Selector="ListBoxItem">
+          <Setter Property="Cursor" Value="{Binding Cursor}"/>
+        </Style>
+      </ListBox.Styles>
+      <ListBox.ItemTemplate>
+        <DataTemplate>
+          <TextBlock Text="{Binding Type}"/>
+        </DataTemplate>
+      </ListBox.ItemTemplate>
+    </ListBox>
+
+    <StackPanel Grid.Column="1" Grid.Row="1" Margin="8 8 0 8">
+      <Button Cursor="{Binding CustomCursor}" Margin="0 8" Padding="16">
+        <TextBlock>Custom Cursor</TextBlock>
+      </Button>
+    </StackPanel>
+ </Grid>
+</UserControl>

+ 20 - 0
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);
+        }
+    }
+}

+ 44 - 0
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Input;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+    public class CursorPageViewModel : ViewModelBase
+    {
+        public CursorPageViewModel()
+        {
+            StandardCursors = Enum.GetValues(typeof(StandardCursorType))
+                .Cast<StandardCursorType>()
+                .Select(x => new StandardCursorModel(x))
+                .ToList();
+
+            var loader = AvaloniaLocator.Current.GetService<IAssetLoader>();
+            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<StandardCursorModel> 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; }
+        }
+    }
+}

+ 6 - 0
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -0,0 +1,6 @@
+Compat issues with assembly Avalonia.Controls:
+MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 4

+ 1 - 1
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)
         {
         }
 

+ 1 - 1
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@@ -98,7 +98,7 @@ namespace Avalonia.Platform
         /// Sets the cursor associated with the toplevel.
         /// </summary>
         /// <param name="cursor">The cursor. Use null for default cursor</param>
-        void SetCursor(IPlatformHandle cursor);
+        void SetCursor(ICursorImpl cursor);
 
         /// <summary>
         /// Gets or sets a method called when the underlying implementation is destroyed.

+ 1 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -165,7 +165,7 @@ namespace Avalonia.Controls
             this.GetObservable(PointerOverElementProperty)
                 .Select(
                     x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
-                .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor));
+                .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformImpl));
 
             if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
             {

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@@ -47,7 +47,7 @@ namespace Avalonia.DesignerSupport.Remote
             var threading = new InternalPlatformThreadingInterface();
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToSingleton<ClipboardStub>()
-                .Bind<IStandardCursorFactory>().ToSingleton<CursorFactoryStub>()
+                .Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(Keyboard)
                 .Bind<IPlatformSettings>().ToConstant(instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(threading)

+ 9 - 3
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)
         {
         }
 
@@ -192,9 +192,15 @@ namespace Avalonia.DesignerSupport.Remote
         public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
     }
 
-    class CursorFactoryStub : IStandardCursorFactory
+    class CursorFactoryStub : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType) => 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

+ 1 - 1
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@@ -58,7 +58,7 @@ namespace Avalonia.Headless
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformThreadingInterface>().ToConstant(new HeadlessPlatformThreadingInterface())
                 .Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
-                .Bind<IStandardCursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
+                .Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
                 .Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub())
                 .Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>()
                 .Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()

+ 5 - 3
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -52,12 +52,14 @@ namespace Avalonia.Headless
         }
     }
 
-    class HeadlessCursorFactoryStub : IStandardCursorFactory
+    class HeadlessCursorFactoryStub : ICursorFactory
     {
+        public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
 
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        private class CursorStub : ICursorImpl
         {
-            return new PlatformHandle(new IntPtr((int)cursorType), "STUB");
+            public void Dispose() { }
         }
     }
 

+ 1 - 1
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)
         {
 
         }

+ 4 - 0
src/Avalonia.Input/ApiCompatBaseline.txt

@@ -0,0 +1,4 @@
+Compat issues with assembly Avalonia.Input:
+MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract.
+Total Issues: 2

+ 18 - 21
src/Avalonia.Input/Cursors.cs → src/Avalonia.Input/Cursor.cs

@@ -1,15 +1,11 @@
 using System;
+using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 
+#nullable enable
+
 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,
@@ -46,21 +42,28 @@ namespace Avalonia.Input
         // SizeNorthEastSouthWest,
     }
 
-    public class Cursor
+    public class Cursor : IDisposable
     {
         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)
-            : this(GetCursor(cursorType))
+            : this(GetCursorFactory().GetCursor(cursorType))
+        {
+        }
+
+        public Cursor(IBitmap cursor, PixelPoint hotSpot)
+            : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot))
         {
         }
 
-        public IPlatformHandle PlatformCursor { get; }
+        public ICursorImpl PlatformImpl { get; }
+
+        public void Dispose() => PlatformImpl.Dispose();
 
         public static Cursor Parse(string s)
         {
@@ -69,16 +72,10 @@ namespace Avalonia.Input
                 throw new ArgumentException($"Unrecognized cursor type '{s}'.");
         }
 
-        private static IPlatformHandle GetCursor(StandardCursorType type)
+        private static ICursorFactory GetCursorFactory()
         {
-            var platform = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
-
-            if (platform == null)
-            {
-                throw new Exception("Could not create Cursor: IStandardCursorFactory not registered.");
-            }
-
-            return platform.GetCursor(type);
+            return AvaloniaLocator.Current.GetService<ICursorFactory>() ??
+                throw new Exception("Could not create Cursor: ICursorFactory not registered.");
         }
     }
 }

+ 12 - 0
src/Avalonia.Input/Platform/ICursorFactory.cs

@@ -0,0 +1,12 @@
+using Avalonia.Input;
+
+#nullable enable
+
+namespace Avalonia.Platform
+{
+    public interface ICursorFactory
+    {
+        ICursorImpl GetCursor(StandardCursorType cursorType);
+        ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot);
+    }
+}

+ 14 - 0
src/Avalonia.Input/Platform/ICursorImpl.cs

@@ -0,0 +1,14 @@
+using System;
+using Avalonia.Input;
+
+#nullable enable
+
+namespace Avalonia.Platform
+{
+    /// <summary>
+    /// Represents a platform implementation of a <see cref="Cursor"/>.
+    /// </summary>
+    public interface ICursorImpl : IDisposable
+    {
+    }
+}

+ 0 - 9
src/Avalonia.Input/Platform/IStandardCursorFactory.cs

@@ -1,9 +0,0 @@
-using Avalonia.Input;
-
-namespace Avalonia.Platform
-{
-    public interface IStandardCursorFactory
-    {
-        IPlatformHandle GetCursor(StandardCursorType cursorType);
-    }
-}

+ 1 - 1
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -97,7 +97,7 @@ namespace Avalonia.Native
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformThreadingInterface>()
                 .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface()))
-                .Bind<IStandardCursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
+                .Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
                 .Bind<IPlatformIconLoader>().ToSingleton<IconLoader>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
                 .Bind<IPlatformSettings>().ToConstant(this)

+ 22 - 3
src/Avalonia.Native/Cursor.cs

@@ -1,11 +1,12 @@
 using System;
+using System.IO;
 using Avalonia.Input;
 using Avalonia.Platform;
 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;
@@ -24,7 +25,7 @@ namespace Avalonia.Native
         }
     }
 
-    class CursorFactory : IStandardCursorFactory
+    class CursorFactory : ICursorFactory
     {
         IAvnCursorFactory _native;
 
@@ -33,10 +34,28 @@ 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 unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
+        {
+            using(var ms = new MemoryStream())
+            {
+                cursor.Save(ms);
+
+                var imageData = ms.ToArray();
+
+                fixed(void* ptr = imageData)
+                {
+                    var avnCursor = _native.CreateCustomCursor(ptr, new IntPtr(imageData.Length),
+                        new AvnPixelSize { Width = hotSpot.X, Height = hotSpot.Y });
+
+                    return new AvaloniaNativeCursor(avnCursor);
+                }
+            }
+        }
     }
 }

+ 3 - 3
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<IKeyboardDevice>();
             _mouse = new MouseDevice();
-            _cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
+            _cursorFactory = AvaloniaLocator.Current.GetService<ICursorFactory>();
         }
 
         protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext)
@@ -398,7 +398,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)
             {

+ 1 - 0
src/Avalonia.Native/avn.idl

@@ -619,6 +619,7 @@ interface IAvnCursor : IUnknown
 interface IAvnCursorFactory : IUnknown
 {
      HRESULT GetCursor(AvnStandardCursorType cursorType, IAvnCursor** retOut);
+     HRESULT CreateCustomCursor (void* bitmapData, size_t length, AvnPixelSize hotPixel, IAvnCursor** retOut);
 }
 
 [uuid(60452465-8616-40af-bc00-042e69828ce7)]

+ 70 - 3
src/Avalonia.X11/X11CursorFactory.cs

@@ -1,12 +1,17 @@
 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
 {
-    class X11CursorFactory : IStandardCursorFactory
+    class X11CursorFactory : ICursorFactory
     {
         private static readonly byte[] NullCursorData = new byte[] { 0 };
 
@@ -51,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)
@@ -64,7 +69,12 @@ namespace Avalonia.X11
                 ? _cursors[shape]
                 : _cursors[CursorFontShape.XC_top_left_arrow];
             }
-            return new PlatformHandle(handle, "XCURSOR");
+            return new CursorImpl(handle);
+        }
+
+        public unsafe ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
+        {
+            return new XImageCursor(_display, cursor, hotSpot);
         }
 
         private static IntPtr GetNullCursor(IntPtr display)
@@ -74,5 +84,62 @@ 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 : CursorImpl, IFramebufferPlatformSurface, IPlatformHandle
+        {
+            private readonly PixelSize _pixelSize;
+            private readonly IUnmanagedBlob _blob;
+
+            public XImageCursor(IntPtr display, IBitmapImpl bitmap, PixelPoint hotSpot)
+            {
+                var size = Marshal.SizeOf<XcursorImage>() +
+                    (bitmap.PixelSize.Width * bitmap.PixelSize.Height * 4);
+
+                _pixelSize = bitmap.PixelSize;
+                _blob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(size);
+                
+                var image = (XcursorImage*)_blob.Address;
+                image->version = 1;
+                image->size = Marshal.SizeOf<XcursorImage>();
+                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<IPlatformRenderInterface>().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 string HandleDescriptor => "XCURSOR";
+
+            public override void Dispose()
+            {
+                XLib.XcursorImageDestroy(Handle);
+                _blob.Dispose();
+            }
+
+            public ILockedFramebuffer Lock()
+            {
+                return new LockedFramebuffer(
+                    _blob.Address + Marshal.SizeOf<XcursorImage>(),
+                    _pixelSize, _pixelSize.Width * 4,
+                    new Vector(96, 96), PixelFormat.Bgra8888, null);
+            }
+        }
+    }
+
+    class CursorImpl : ICursorImpl
+    {
+        public CursorImpl() { }
+        public CursorImpl(IntPtr handle) => Handle = handle;
+        public IntPtr Handle { get; protected set; }
+        public virtual void Dispose() { }
     }
 }

+ 1 - 1
src/Avalonia.X11/X11Platform.cs

@@ -71,7 +71,7 @@ namespace Avalonia.X11
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
                 .Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)
-                .Bind<IStandardCursorFactory>().ToConstant(new X11CursorFactory(Display))
+                .Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
                 .Bind<IClipboard>().ToConstant(new X11Clipboard(this))
                 .Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
                 .Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))

+ 1 - 1
src/Avalonia.X11/X11Structs.cs

@@ -1693,7 +1693,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 */

+ 3 - 5
src/Avalonia.X11/X11Window.cs

@@ -872,15 +872,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);
             }
         }
 

+ 7 - 0
src/Avalonia.X11/XLib.cs

@@ -20,6 +20,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);
@@ -569,6 +570,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);

+ 1 - 1
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)
         {
         }
 

+ 1 - 1
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -38,7 +38,7 @@ namespace Avalonia.LinuxFramebuffer
                 .Bind<IPlatformThreadingInterface>().ToConstant(Threading)
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
-                .Bind<IStandardCursorFactory>().ToTransient<CursorFactoryStub>()
+                .Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())

+ 6 - 3
src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs

@@ -4,11 +4,14 @@ using Avalonia.Platform;
 
 namespace Avalonia.LinuxFramebuffer
 {
-    internal class CursorFactoryStub : IStandardCursorFactory
+    internal class CursorFactoryStub : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
+
+        private class CursorStub : ICursorImpl
         {
-            return new PlatformHandle(IntPtr.Zero, null);
+            public void Dispose() { }
         }
     }
     internal class PlatformSettings : IPlatformSettings

+ 3 - 3
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<RawInputEventArgs> ITopLevelImpl.Input { get; set; } //TODO

+ 114 - 13
src/Windows/Avalonia.Win32/CursorFactory.cs

@@ -1,12 +1,17 @@
 using System;
 using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.IO;
 using Avalonia.Input;
 using Avalonia.Platform;
 using Avalonia.Win32.Interop;
+using SdBitmap = System.Drawing.Bitmap;
+using SdPixelFormat = System.Drawing.Imaging.PixelFormat;
 
 namespace Avalonia.Win32
 {
-    internal class CursorFactory : IStandardCursorFactory
+    internal class CursorFactory : ICursorFactory
     {
         public static CursorFactory Instance { get; } = new CursorFactory();
 
@@ -29,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));
                 }
             }
         }
@@ -70,22 +74,119 @@ namespace Avalonia.Win32
             {StandardCursorType.DragLink, 32516},
         };
 
-        private static readonly Dictionary<StandardCursorType, IPlatformHandle> Cache =
-            new Dictionary<StandardCursorType, IPlatformHandle>();
+        private static readonly Dictionary<StandardCursorType, CursorImpl> Cache =
+            new Dictionary<StandardCursorType, CursorImpl>();
 
-        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 ICursorImpl 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 CursorImpl(UnmanagedMethods.CreateIconIndirect(ref info), true);
+        }
+
+        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.Format32bppPArgb)
+            {
+                throw new NotSupportedException(
+                    "Images with premultiplied alpha not yet supported as cursor images.");
+            }
+
+            if (source.PixelFormat != SdPixelFormat.Format32bppArgb)
+            {
+                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);
+            }
+        }
+    }
+
+    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;
+            }
+        }
     }
 }

+ 16 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1035,6 +1035,12 @@ 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 DestroyIcon(IntPtr hIcon);
+
         [DllImport("user32.dll")]
         public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
 
@@ -1762,6 +1768,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
         {

+ 1 - 1
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -104,7 +104,7 @@ namespace Avalonia.Win32
             Options = options;
             AvaloniaLocator.CurrentMutable
                 .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
-                .Bind<IStandardCursorFactory>().ToConstant(CursorFactory.Instance)
+                .Bind<ICursorFactory>().ToConstant(CursorFactory.Instance)
                 .Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
                 .Bind<IPlatformSettings>().ToConstant(s_instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(s_instance)

+ 10 - 5
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -608,14 +608,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);
+                }
             }
         }
 

+ 2 - 2
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -74,7 +74,7 @@ namespace Avalonia.iOS
 
             public PixelPoint PointToScreen(Point point) => new PixelPoint((int) point.X, (int) point.Y);
 
-            public void SetCursor(IPlatformHandle cursor)
+            public void SetCursor(ICursorImpl _)
             {
                 // no-op
             }
@@ -136,4 +136,4 @@ namespace Avalonia.iOS
             set => _topLevel.Content = value;
         }
     }
-}
+}

+ 1 - 1
src/iOS/Avalonia.iOS/Platform.cs

@@ -27,7 +27,7 @@ namespace Avalonia.iOS
             var softKeyboard = new SoftKeyboardHelper();
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformOpenGlInterface>().ToConstant(GlFeature)
-                .Bind<IStandardCursorFactory>().ToConstant(new CursorFactoryStub())
+                .Bind<ICursorFactory>().ToConstant(new CursorFactoryStub())
                 .Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
                 .Bind<IClipboard>().ToConstant(new ClipboardImpl())
                 .Bind<IPlatformSettings>().ToConstant(new PlatformSettings())

+ 9 - 3
src/iOS/Avalonia.iOS/Stubs.cs

@@ -5,9 +5,15 @@ using Avalonia.Platform;
 
 namespace Avalonia.iOS
 {
-    class CursorFactoryStub : IStandardCursorFactory
+    class CursorFactoryStub : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "NULL");
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorImplStub();
+        ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new CursorImplStub();
+
+        private class CursorImplStub : ICursorImpl
+        {
+            public void Dispose() { }
+        }
     }
 
     class WindowingPlatformStub : IWindowingPlatform
@@ -57,4 +63,4 @@ namespace Avalonia.iOS
             _ms.CopyTo(outputStream);
         }
     }
-}
+}

+ 6 - 3
tests/Avalonia.Benchmarks/NullCursorFactory.cs

@@ -4,11 +4,14 @@ using Avalonia.Platform;
 
 namespace Avalonia.Benchmarks
 {
-    internal class NullCursorFactory : IStandardCursorFactory
+    internal class NullCursorFactory : ICursorFactory
     {
-        public IPlatformHandle GetCursor(StandardCursorType cursorType)
+        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new NullCursorImpl();
+        ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new NullCursorImpl();
+
+        private class NullCursorImpl : ICursorImpl
         {
-            return new PlatformHandle(IntPtr.Zero, "null");
+            public void Dispose() { }
         }
     }
 }

+ 1 - 1
tests/Avalonia.Controls.UnitTests/CalendarDatePickerTests.cs

@@ -74,7 +74,7 @@ namespace Avalonia.Controls.UnitTests
         }
 
         private static TestServices Services => TestServices.MockThreadingInterface.With(
-            standardCursorFactory: Mock.Of<IStandardCursorFactory>());
+            standardCursorFactory: Mock.Of<ICursorFactory>());
 
         private CalendarDatePicker CreateControl()
         {

+ 15 - 4
tests/Avalonia.Controls.UnitTests/CursorFactoryMock.cs

@@ -1,14 +1,25 @@
-using System;
 using Avalonia.Input;
 using Avalonia.Platform;
 
 namespace Avalonia.Controls.UnitTests
 {
-    public class CursorFactoryMock : IStandardCursorFactory
+    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 ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
+        {
+            return new MockCursorImpl();
+        }
+
+        private class MockCursorImpl : ICursorImpl
+        {
+            public void Dispose()
+            {
+            }
         }
     }
 }

+ 1 - 1
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<IStandardCursorFactory>(),
+            standardCursorFactory: Mock.Of<ICursorFactory>(),
             textShaperImpl: new MockTextShaperImpl());
 
         private IControlTemplate CreateTemplate()

+ 2 - 2
tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs

@@ -11,8 +11,8 @@ namespace Avalonia.Controls.UnitTests
     {
         public GridSplitterTests()
         {
-            var cursorFactoryImpl = new Mock<IStandardCursorFactory>();
-            AvaloniaLocator.CurrentMutable.Bind<IStandardCursorFactory>().ToConstant(cursorFactoryImpl.Object);
+            var cursorFactoryImpl = new Mock<ICursorFactory>();
+            AvaloniaLocator.CurrentMutable.Bind<ICursorFactory>().ToConstant(cursorFactoryImpl.Object);
         }
 
         [Fact]

+ 2 - 2
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<IStandardCursorFactory>());
+            standardCursorFactory: Mock.Of<ICursorFactory>());
 
         private static TestServices Services => TestServices.MockThreadingInterface.With(
-            standardCursorFactory: Mock.Of<IStandardCursorFactory>());
+            standardCursorFactory: Mock.Of<ICursorFactory>());
 
         private IControlTemplate CreateTemplate()
         {

+ 1 - 1
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<IStandardCursorFactory>());
+            standardCursorFactory: Mock.Of<ICursorFactory>());
 
         private IControlTemplate CreateTemplate()
         {

+ 1 - 1
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<IStandardCursorFactory>(),
+            standardCursorFactory: Mock.Of<ICursorFactory>(),
             textShaperImpl: new MockTextShaperImpl());
 
         private IControlTemplate CreateTemplate()

+ 4 - 4
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<IStandardCursorFactory>(),
+            standardCursorFactory: Mock.Of<ICursorFactory>(),
             styler: new Styler(),
             theme: () => CreateDefaultTheme(),
             threadingInterface: Mock.Of<IPlatformThreadingInterface>(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<Styles> 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<Styles> 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<Styles> theme = null,
             IPlatformThreadingInterface threadingInterface = null,

+ 1 - 1
tests/Avalonia.UnitTests/UnitTestApplication.cs

@@ -62,7 +62,7 @@ namespace Avalonia.UnitTests
                 .Bind<ITextShaperImpl>().ToConstant(Services.TextShaperImpl)
                 .Bind<IPlatformThreadingInterface>().ToConstant(Services.ThreadingInterface)
                 .Bind<IScheduler>().ToConstant(Services.Scheduler)
-                .Bind<IStandardCursorFactory>().ToConstant(Services.StandardCursorFactory)
+                .Bind<ICursorFactory>().ToConstant(Services.StandardCursorFactory)
                 .Bind<IStyler>().ToConstant(Services.Styler)
                 .Bind<IWindowingPlatform>().ToConstant(Services.WindowingPlatform)
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();