Add support for loading custom mouse cursor images

This commit is contained in:
2026-04-24 11:20:04 +02:00
parent 804315bb3e
commit 1988fae929
10 changed files with 267 additions and 173 deletions
+10 -2
View File
@@ -20,6 +20,7 @@ namespace
Nullable<bool> Fullscreen;
Nullable<Float2> Size;
bool CursorVisible = true;
CursorType CachedCursorType = CursorType::Default;
CursorLockMode CursorLock = CursorLockMode::None;
CursorLockMode PendingCursorLock = CursorLockMode::None;
bool LastGameViewportFocus = false;
@@ -107,10 +108,17 @@ void Screen::SetCursorVisible(const bool value)
#else
const auto win = Engine::MainWindow;
#endif
if (!value && win)
{
// Cache cursor used before hiding it to restore it later (eg. if game uses image cursor and hides it for a while)
CachedCursorType = win->GetCursor();
if (CachedCursorType == CursorType::Hidden)
CachedCursorType = CursorType::Default;
}
if (win && Engine::HasGameViewportFocus())
win->SetCursor(value ? CursorType::Default : CursorType::Hidden);
win->SetCursor(value ? CachedCursorType : CursorType::Hidden);
else if (win)
win->SetCursor(CursorType::Default);
win->SetCursor(CachedCursorType);
CursorVisible = value;
}
+7 -2
View File
@@ -36,7 +36,7 @@ API_ENUM() enum class ClosingReason
API_ENUM() enum class CursorType
{
/// <summary>
/// The default.
/// The default cursor, usually an arrow.
/// </summary>
Default = 0,
@@ -56,7 +56,7 @@ API_ENUM() enum class CursorType
Help,
/// <summary>
/// The I beam.
/// The I-beam for text selection.
/// </summary>
IBeam,
@@ -100,6 +100,11 @@ API_ENUM() enum class CursorType
/// </summary>
Hidden,
/// <summary>
/// The custom cursor image. Loaded manually via Window::LoadCursorImage.
/// </summary>
Image,
MAX
};
+50 -3
View File
@@ -23,7 +23,7 @@ API_INJECT_CODE(cpp, "#include \"Engine/Platform/Window.h\"");
/// <summary>
/// Native platform window object.
/// </summary>
API_CLASS(NoSpawn, NoConstructor, Sealed, Name="Window")
API_CLASS(NoSpawn, NoConstructor, Sealed, Name="Window", Tag="NativeInvokeUseName")
class FLAXENGINE_API WindowBase : public ScriptingObject
{
DECLARE_SCRIPTING_TYPE_NO_SPAWN(WindowBase);
@@ -31,16 +31,17 @@ class FLAXENGINE_API WindowBase : public ScriptingObject
protected:
bool _visible, _minimized, _maximized, _isClosing, _showAfterFirstPaint, _focused;
GPUSwapChain* _swapChain;
void* _cursorImage = nullptr;
CreateWindowSettings _settings;
String _title;
CursorType _cursor;
Float2 _clientSize;
int _dpi;
int32 _dpi;
float _dpiScale;
Float2 _trackingMouseOffset;
bool _isUsingMouseOffset = false;
Rectangle _mouseOffsetScreenSize;
bool _isUsingMouseOffset = false;
bool _isTrackingMouse = false;
bool _isHorizontalFlippingMouse = false;
bool _isVerticalFlippingMouse = false;
@@ -546,6 +547,52 @@ public:
_cursor = type;
}
/// <summary>
/// Gets the mouse cursor image handle.
/// </summary>
API_PROPERTY() FORCE_INLINE void* GetCursorImage() const
{
return _cursorImage;
}
/// <summary>
/// Sets the mouse cursor image handle.
/// </summary>
/// <param name="image">The cursor image.</param>
API_PROPERTY() virtual void SetCursorImage(void* image)
{
_cursorImage = image;
}
/// <summary>
/// Loads a cursor file as a cursor image. Returns a native cursor handle that can be used with `SetCursorImage`. Support depends on platform (eg. Windows uses .cur/.ani files).
/// </summary>
/// <param name="path">Path to the cursor file (absolute or relative).</param>
/// <returns>Loaded cursor image handle, or null if failed.</returns>
API_FUNCTION() static void* LoadCursorImage(const StringView& path)
{
return nullptr;
}
/// <summary>
/// Loads a texture as a cursor image. Returns a native cursor handle that can be used with `SetCursorImage`. Support depends on platform.
/// </summary>
/// <param name="image">Texture data with a cursor image.</param>
/// <param name="hotSpot">The texture coordinate of a cursor's hot spot that defines the exact 'clickable' point.</param>
/// <returns>Loaded cursor image handle, or null if failed.</returns>
API_FUNCTION() static void* LoadCursorImage(const TextureData& image, const Int2& hotSpot = Int2::Zero)
{
return nullptr;
}
/// <summary>
/// Destroys a cursor image loaded with `LoadCursorImage`. Should be called to release native resources after the cursor image is not needed anymore.
/// </summary>
/// <param name="image">The cursor image.</param>
API_FUNCTION() static void DestroyCursorImage(void* image)
{
}
/// <summary>
/// Sets the window icon.
/// </summary>
+23 -55
View File
@@ -230,64 +230,32 @@ void GDKWindow::UpdateCursor() const
return;
}
int32 index = 0;
switch (_cursor)
const LPCWSTR cursors[] =
{
case CursorType::Default:
break;
case CursorType::Cross:
index = 1;
break;
case CursorType::Hand:
index = 2;
break;
case CursorType::Help:
index = 3;
break;
case CursorType::IBeam:
index = 4;
break;
case CursorType::No:
index = 5;
break;
case CursorType::Wait:
index = 11;
break;
case CursorType::SizeAll:
index = 6;
break;
case CursorType::SizeNESW:
index = 7;
break;
case CursorType::SizeNS:
index = 8;
break;
case CursorType::SizeNWSE:
index = 9;
break;
case CursorType::SizeWE:
index = 10;
break;
}
static const LPCWSTR cursors[] =
{
IDC_ARROW,
IDC_CROSS,
IDC_HAND,
IDC_HELP,
IDC_IBEAM,
IDC_NO,
IDC_SIZEALL,
IDC_SIZENESW,
IDC_SIZENS,
IDC_SIZENWSE,
IDC_SIZEWE,
IDC_WAIT,
IDC_ARROW, // Default
IDC_CROSS, // Cross
IDC_HAND, // Hand
IDC_HELP, // Help
IDC_IBEAM, // IBeam
IDC_NO, // No
IDC_WAIT, // Wait
IDC_SIZEALL, // SizeAll
IDC_SIZENESW, // SizeNESW
IDC_SIZENS, // SizeNS
IDC_SIZENWSE, // SizeNWSE
IDC_SIZEWE, // SizeWE
};
static_assert(ARRAY_COUNT(cursors) + 2 == (int32)CursorType::MAX, "Invalid cursors count.");
HCURSOR cursor;
if (_cursor == CursorType::Image)
{
LOG(Error, "GDK doesn't support hardware image cursors");
cursor = NULL;
}
else
cursor = LoadCursorW(nullptr, cursors[(int32)_cursor]);
ASSERT(index >= 0 && index < ARRAY_COUNT(cursors));
const HCURSOR cursor = LoadCursorW(nullptr, cursors[index]);
::SetCursor(cursor);
}
+1 -1
View File
@@ -846,7 +846,7 @@ void LinuxWindow::SetCursor(CursorType type)
WindowBase::SetCursor(type);
LINUX_WINDOW_PROLOG;
if (!display)
if (!display || type == CursorType::Image)
return;
X11::XDefineCursor(display, window, Cursors[(int32)type]);
}
+2
View File
@@ -1197,6 +1197,8 @@ void MacWindow::SetCursor(CursorType type)
case CursorType::Hidden:
[NSCursor hide];
return;
case CursorType::Image:
// TODO: custom cursor support
default:
cursor = [NSCursor arrowCursor];
break;
+67 -43
View File
@@ -995,50 +995,42 @@ void SDLWindow::UpdateCursor()
//if (_isTrackingMouse)
// Input::Mouse->SetRelativeMode(false, this);
int32 index = SDL_SYSTEM_CURSOR_DEFAULT;
switch (_cursor)
const SDL_SystemCursor cursors[] =
{
case CursorType::Cross:
index = SDL_SYSTEM_CURSOR_CROSSHAIR;
break;
case CursorType::Hand:
index = SDL_SYSTEM_CURSOR_POINTER;
break;
case CursorType::Help:
//index = SDL_SYSTEM_CURSOR_DEFAULT;
break;
case CursorType::IBeam:
index = SDL_SYSTEM_CURSOR_TEXT;
break;
case CursorType::No:
index = SDL_SYSTEM_CURSOR_NOT_ALLOWED;
break;
case CursorType::Wait:
index = SDL_SYSTEM_CURSOR_WAIT;
break;
case CursorType::SizeAll:
index = SDL_SYSTEM_CURSOR_MOVE;
break;
case CursorType::SizeNESW:
index = SDL_SYSTEM_CURSOR_NESW_RESIZE;
break;
case CursorType::SizeNS:
index = SDL_SYSTEM_CURSOR_NS_RESIZE;
break;
case CursorType::SizeNWSE:
index = SDL_SYSTEM_CURSOR_NWSE_RESIZE;
break;
case CursorType::SizeWE:
index = SDL_SYSTEM_CURSOR_EW_RESIZE;
break;
case CursorType::Default:
default:
break;
}
SDL_SYSTEM_CURSOR_DEFAULT, // Default
SDL_SYSTEM_CURSOR_CROSSHAIR, // Cross
SDL_SYSTEM_CURSOR_POINTER, // Hand
SDL_SYSTEM_CURSOR_DEFAULT, // Help
SDL_SYSTEM_CURSOR_TEXT, // IBeam
SDL_SYSTEM_CURSOR_NOT_ALLOWED, // No
SDL_SYSTEM_CURSOR_WAIT, // Wait
SDL_SYSTEM_CURSOR_MOVE, // SizeAll
SDL_SYSTEM_CURSOR_NESW_RESIZE, // SizeNESW
SDL_SYSTEM_CURSOR_NS_RESIZE, // SizeNS
SDL_SYSTEM_CURSOR_NWSE_RESIZE, // SizeNWSE
SDL_SYSTEM_CURSOR_EW_RESIZE, // SizeWE
};
static_assert(ARRAY_COUNT(cursors) + 2 == (int32)CursorType::MAX, "Invalid cursors count.");
if (SDLImpl::Cursors[index] == nullptr)
SDLImpl::Cursors[index] = SDL_CreateSystemCursor(static_cast<SDL_SystemCursor>(index));
SDL_SetCursor(SDLImpl::Cursors[index]);
SDL_Cursor* cursor;
if (_cursor == CursorType::Image)
{
static bool Once = true;
if (!_cursorImage && Once)
{
Once = false;
LOG(Error, "Missing cursor image to set.");
}
cursor = (SDL_Cursor*)_cursorImage;
}
else
{
int32 index = cursors[(int32)_cursor];
if (SDLImpl::Cursors[index] == nullptr)
SDLImpl::Cursors[index] = SDL_CreateSystemCursor(static_cast<SDL_SystemCursor>(index));
cursor = SDLImpl::Cursors[index];
}
SDL_SetCursor(cursor);
}
void SDLWindow::SetIcon(TextureData& icon)
@@ -1046,10 +1038,42 @@ void SDLWindow::SetIcon(TextureData& icon)
Array<Color32> colorData;
icon.GetPixels(colorData);
SDL_Surface* surface = SDL_CreateSurfaceFrom(icon.Width, icon.Height, SDL_PIXELFORMAT_ABGR8888, colorData.Get(), sizeof(Color32) * icon.Width);
SDL_SetWindowIcon(_window, surface);
SDL_free(surface);
}
void SDLWindow::SetCursorImage(void* image)
{
WindowBase::SetCursorImage(image);
if (_cursor == CursorType::Image)
UpdateCursor();
}
void* SDLWindow::LoadCursorImage(const StringView& path)
{
return nullptr;
}
void* SDLWindow::LoadCursorImage(const TextureData& image, const Int2& hotSpot)
{
Array<Color32> pixels;
if (image.GetPixels(pixels))
{
LOG(Error, "Invalid cursor texture");
return nullptr;
}
SDL_Surface* surface = SDL_CreateSurfaceFrom(image.Width, image.Height, SDL_PIXELFORMAT_ABGR8888, pixels.Get(), sizeof(Color32) * image.Width);
SDL_Cursor* result = SDL_CreateColorCursor(surface, hotSpot.X, hotSpot.Y);
SDL_free(surface);
return result;
}
void SDLWindow::DestroyCursorImage(void* image)
{
if (image)
SDL_DestroyCursor((SDL_Cursor*)image);
}
#endif
+4
View File
@@ -108,6 +108,10 @@ public:
void SetMousePosition(const Float2& position) const override;
void SetCursor(CursorType type) override;
void SetIcon(TextureData& icon) override;
void SetCursorImage(void* image) override;
static void* LoadCursorImage(const StringView& path);
static void* LoadCursorImage(const TextureData& image, const Int2& hotSpot = Int2::Zero);
static void DestroyCursorImage(void* image);
#if USE_EDITOR && PLATFORM_WINDOWS
// [IUnknown]
@@ -7,9 +7,11 @@
#include "WindowsInput.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Math/Color32.h"
#include "Engine/Graphics/GPUSwapChain.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/Textures/TextureData.h"
#include "../Win32/IncludeWindowsHeaders.h"
#include <propidl.h>
#if USE_EDITOR
@@ -696,6 +698,71 @@ void WindowsWindow::SetCursor(CursorType type)
UpdateCursor();
}
void WindowsWindow::SetCursorImage(void* image)
{
// Base
WindowBase::SetCursorImage(image);
if (_cursor == CursorType::Image)
UpdateCursor();
}
void* WindowsWindow::LoadCursorImage(const StringView& path)
{
return ::LoadCursorFromFileW(path.GetText());
}
// From wingdi.h
WIN_API HBITMAP WIN_API_CALLCONV CreateBitmap(int nWidth, int nHeight, UINT nPlanes, UINT nBitCount, CONST VOID* lpBits);
WIN_API BOOL WIN_API_CALLCONV DeleteObject(HGDIOBJ ho);
#pragma comment(lib, "Gdi32.lib")
void* WindowsWindow::LoadCursorImage(const TextureData& image, const Int2& hotSpot)
{
// Get image pixels
Array<Color32> pixels;
if (image.GetPixels(pixels))
{
LOG(Error, "Invalid cursor texture");
return nullptr;
}
// RGBA -> BGRA
for (int32 y = 0; y < image.Height; y++)
{
for (int32 x = 0; x < image.Width; x++)
{
auto& color = *(pixels.Get() + y * image.Width + x);
color = Color32(color.B, color.G, color.R, color.A);
}
}
// Initialize a dummy mask
Array<uint8> pixelsMask;
pixelsMask.AddUninitialized(pixels.Count());
Platform::MemorySet(pixelsMask.Get(), pixelsMask.Count(), 255);
// Create cursor from the image
HBITMAP colorBitmap = ::CreateBitmap(image.Width, image.Height, 1, 32, pixels.Get());
HBITMAP maskBitmap = ::CreateBitmap(image.Width, image.Height, 1, 8, pixelsMask.Get());
ICONINFO iconInfo = {};
iconInfo.xHotspot = hotSpot.X;
iconInfo.yHotspot = hotSpot.Y;
iconInfo.hbmColor = colorBitmap;
iconInfo.hbmMask = maskBitmap;
HCURSOR result = ::CreateIconIndirect(&iconInfo);
::DeleteObject(colorBitmap);
::DeleteObject(maskBitmap);
return result;
}
void WindowsWindow::DestroyCursorImage(void* image)
{
if (!image)
return;
::DestroyCursor((HCURSOR)image);
}
void WindowsWindow::CheckForWindowResize()
{
// Skip for minimized window (GetClientRect for minimized window returns 0)
@@ -763,76 +830,49 @@ void WindowsWindow::UpdateCursor()
else if (_lastCursorHidden)
{
_lastCursorHidden = false;
while(::ShowCursor(TRUE) < 0)
while (::ShowCursor(TRUE) < 0)
{
if (_cursorHiddenSafetyCount >= 100)
{
LOG(Warning, "Cursor has failed to show.");
break;
}
_cursorHiddenSafetyCount += 1;
_cursorHiddenSafetyCount++;
}
_cursorHiddenSafetyCount = 0;
}
int32 index = 0;
switch (_cursor)
const LPCWSTR cursors[] =
{
case CursorType::Default:
break;
case CursorType::Cross:
index = 1;
break;
case CursorType::Hand:
index = 2;
break;
case CursorType::Help:
index = 3;
break;
case CursorType::IBeam:
index = 4;
break;
case CursorType::No:
index = 5;
break;
case CursorType::Wait:
index = 11;
break;
case CursorType::SizeAll:
index = 6;
break;
case CursorType::SizeNESW:
index = 7;
break;
case CursorType::SizeNS:
index = 8;
break;
case CursorType::SizeNWSE:
index = 9;
break;
case CursorType::SizeWE:
index = 10;
break;
}
static const LPCWSTR cursors[] =
{
IDC_ARROW,
IDC_CROSS,
IDC_HAND,
IDC_HELP,
IDC_IBEAM,
IDC_NO,
IDC_SIZEALL,
IDC_SIZENESW,
IDC_SIZENS,
IDC_SIZENWSE,
IDC_SIZEWE,
IDC_WAIT,
IDC_ARROW, // Default
IDC_CROSS, // Cross
IDC_HAND, // Hand
IDC_HELP, // Help
IDC_IBEAM, // IBeam
IDC_NO, // No
IDC_WAIT, // Wait
IDC_SIZEALL, // SizeAll
IDC_SIZENESW, // SizeNESW
IDC_SIZENS, // SizeNS
IDC_SIZENWSE, // SizeNWSE
IDC_SIZEWE, // SizeWE
};
static_assert(ARRAY_COUNT(cursors) + 2 == (int32)CursorType::MAX, "Invalid cursors count.");
HCURSOR cursor;
if (_cursor == CursorType::Image)
{
static bool Once = true;
if (!_cursorImage && Once)
{
Once = false;
LOG(Error, "Missing cursor image to set.");
}
cursor = (HCURSOR)_cursorImage;
}
else
cursor = LoadCursorW(nullptr, cursors[(int32)_cursor]);
ASSERT(index >= 0 && index < ARRAY_COUNT(cursors));
const HCURSOR cursor = LoadCursorW(nullptr, cursors[index]);
::SetCursor(cursor);
}
+6 -10
View File
@@ -19,7 +19,6 @@ class FLAXENGINE_API WindowsWindow : public WindowBase
friend WindowsPlatform;
private:
Windows::HWND _handle;
#if USE_EDITOR
Windows::ULONG _refCount;
@@ -29,16 +28,15 @@ private:
bool _trackingMouse = false;
bool _clipCursorSet = false;
bool _lastCursorHidden = false;
int _cursorHiddenSafetyCount = 0;
float _opacity = 1.0f;
bool _isDuringMaximize = false;
int32 _cursorHiddenSafetyCount = 0;
float _opacity = 1.0f;
Windows::HANDLE _monitor = nullptr;
Windows::LONG _clipCursorRect[4];
int32 _regionWidth = 0, _regionHeight = 0;
Float2 _minimizedScreenPosition = Float2::Zero;
public:
/// <summary>
/// Initializes a new instance of the <see cref="WindowsWindow"/> class.
/// </summary>
@@ -51,7 +49,6 @@ public:
~WindowsWindow();
public:
/// <summary>
/// Gets the window handle.
/// </summary>
@@ -80,7 +77,6 @@ public:
void GetScreenInfo(int32& x, int32& y, int32& width, int32& height) const;
public:
/// <summary>
/// The Windows messages procedure.
/// </summary>
@@ -91,13 +87,11 @@ public:
Windows::LRESULT WndProc(Windows::UINT msg, Windows::WPARAM wParam, Windows::LPARAM lParam);
private:
void CheckForWindowResize();
void UpdateCursor();
void UpdateRegion();
public:
// [WindowBase]
void* GetNativePtr() const override;
void Show() override;
@@ -130,9 +124,12 @@ public:
void StartClippingCursor(const Rectangle& bounds) override;
void EndClippingCursor() override;
void SetCursor(CursorType type) override;
void SetCursorImage(void* image) override;
static void* LoadCursorImage(const StringView& path);
static void* LoadCursorImage(const TextureData& image, const Int2& hotSpot = Int2::Zero);
static void DestroyCursorImage(void* image);
#if USE_EDITOR
// [IUnknown]
Windows::HRESULT __stdcall QueryInterface(const Windows::IID& id, void** ppvObject) override;
Windows::ULONG __stdcall AddRef() override;
@@ -143,7 +140,6 @@ public:
Windows::HRESULT __stdcall DragOver(Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) override;
Windows::HRESULT __stdcall DragLeave() override;
Windows::HRESULT __stdcall Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) override;
#endif
};