Browse Source

Initial support for custom cursors on Win32.

Steven Kirk 5 years ago
parent
commit
03a18f56bb

+ 1 - 0
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

+ 5 - 1
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

+ 10 - 10
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<IStandardCursorFactory>();
-
-            if (platform == null)
-            {
-                throw new Exception("Could not create Cursor: IStandardCursorFactory not registered.");
-            }
-
-            return platform.GetCursor(type);
+            return AvaloniaLocator.Current.GetService<IStandardCursorFactory>() ??
+                throw new Exception("Could not create Cursor: ICursorFactory not registered.");
         }
     }
 }

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

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

+ 5 - 0
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();

+ 5 - 0
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
     {

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

+ 13 - 0
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
         {

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