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