Skip to content

Commit 6ce7ad6

Browse files
committed
Add custom image cursors
1 parent b23ea2f commit 6ce7ad6

9 files changed

Lines changed: 154 additions & 1 deletion

File tree

‎Engine/system/api/macosapi.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class MacOSApi final: SystemApi {
3030

3131
void implSetCursorPosition(SystemApi::Window *w, int x, int y) override;
3232
void implShowCursor(SystemApi::Window *w, CursorShape show) override;
33+
void implSetCustomCursor(const Pixmap& image, int hotX, int hotY) override;
3334

3435
void implSetWindowTitle(SystemApi::Window *w, const char* utf8) override;
3536
float implUiScale(SystemApi::Window* w) override;

‎Engine/system/api/macosapi.mm‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
#include <AppKit/AppKit.h>
77

88
#include <Tempest/Window>
9+
#include <Tempest/Pixmap>
910
#include <Tempest/Log>
1011
#include <Tempest/TextCodec>
1112

13+
#include <cstring>
14+
1215
using namespace Tempest;
1316

1417
static const uint keyTable[26]={
@@ -115,6 +118,8 @@ static NSPoint mousePos(NSPoint px, NSWindow* wnd, bool& inWindow) {
115118
return Tempest::Point{int(px.x), int(px.y)};
116119
}
117120

121+
static NSCursor* customCursor = nil;
122+
118123
static NSCursor* toNSCursor(CursorShape show) {
119124
switch (show) {
120125
case CursorShape::Arrow:
@@ -132,6 +137,8 @@ static NSPoint mousePos(NSPoint px, NSWindow* wnd, bool& inWindow) {
132137
case CursorShape::SizeFDiag:
133138
case CursorShape::SizeAll:
134139
return [NSCursor arrowCursor];
140+
case CursorShape::Custom:
141+
return customCursor!=nil ? customCursor : [NSCursor arrowCursor];
135142
}
136143
return [NSCursor arrowCursor];
137144
}
@@ -439,6 +446,38 @@ - (void)dispatchRenderer{
439446
::implShowCursor(w, show);
440447
}
441448

449+
void MacOSApi::implSetCustomCursor(const Pixmap& image, int hotX, int hotY) {
450+
if(customCursor!=nil) {
451+
[customCursor release];
452+
customCursor = nil;
453+
}
454+
if(image.isEmpty() || image.format()!=TextureFormat::RGBA8)
455+
return;
456+
457+
const NSInteger w = NSInteger(image.w());
458+
const NSInteger h = NSInteger(image.h());
459+
NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nullptr
460+
pixelsWide:w
461+
pixelsHigh:h
462+
bitsPerSample:8
463+
samplesPerPixel:4
464+
hasAlpha:YES
465+
isPlanar:NO
466+
colorSpaceName:NSDeviceRGBColorSpace
467+
bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
468+
bytesPerRow:w*4
469+
bitsPerPixel:32];
470+
std::memcpy([rep bitmapData], image.data(), size_t(w*h*4));
471+
472+
// Image pixels are physical; NSCursor sizes and hot spots are in points
473+
const CGFloat scale = [[NSScreen mainScreen] backingScaleFactor];
474+
NSImage* img = [[NSImage alloc] initWithSize:NSMakeSize(CGFloat(w)/scale, CGFloat(h)/scale)];
475+
[img addRepresentation:rep];
476+
customCursor = [[NSCursor alloc] initWithImage:img hotSpot:NSMakePoint(CGFloat(hotX)/scale, CGFloat(hotY)/scale)];
477+
[img release];
478+
[rep release];
479+
}
480+
442481
void MacOSApi::implSetWindowTitle(Window* w, const char* utf8) {
443482
NSWindow* wnd = reinterpret_cast<NSWindow*>(w);
444483
NSString* str = [NSString stringWithUTF8String:utf8];

‎Engine/system/api/windowsapi.cpp‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#ifdef __WINDOWS__
44

55
#include <Tempest/Event>
6+
#include <Tempest/Pixmap>
67
#include <Tempest/Except>
78
#include <Tempest/TextCodec>
89
#include <Tempest/Window>
@@ -328,7 +329,13 @@ void WindowsApi::implSetCursorPosition(SystemApi::Window *w, int x, int y) {
328329
}
329330
}
330331

332+
static HCURSOR customCursor = nullptr;
333+
331334
static void SetCursor(CursorShape show) {
335+
if(show==CursorShape::Custom && customCursor!=nullptr) {
336+
SetCursor(customCursor);
337+
return;
338+
}
332339
LPSTR idc = IDC_ARROW;
333340
switch(show) {
334341
case Tempest::CursorShape::Arrow:
@@ -355,6 +362,9 @@ static void SetCursor(CursorShape show) {
355362
case Tempest::CursorShape::SizeAll:
356363
idc = IDC_SIZEALL;
357364
break;
365+
case Tempest::CursorShape::Custom:
366+
idc = IDC_ARROW;
367+
break;
358368
}
359369
HCURSOR hcur = idc==nullptr ? nullptr : LoadCursorA(nullptr, idc);
360370
SetCursor(hcur);
@@ -364,6 +374,60 @@ void WindowsApi::implShowCursor(SystemApi::Window*, CursorShape show) {
364374
SetCursor(show);
365375
}
366376

377+
void WindowsApi::implSetCustomCursor(const Pixmap& image, int hotX, int hotY) {
378+
if(customCursor!=nullptr) {
379+
DestroyCursor(customCursor);
380+
customCursor = nullptr;
381+
}
382+
if(image.isEmpty() || image.format()!=TextureFormat::RGBA8)
383+
return;
384+
385+
const int w = int(image.w());
386+
const int h = int(image.h());
387+
388+
BITMAPV5HEADER bi = {};
389+
bi.bV5Size = sizeof(bi);
390+
bi.bV5Width = w;
391+
bi.bV5Height = -h; // top-down rows, as in Pixmap
392+
bi.bV5Planes = 1;
393+
bi.bV5BitCount = 32;
394+
bi.bV5Compression = BI_BITFIELDS;
395+
bi.bV5RedMask = 0x00FF0000;
396+
bi.bV5GreenMask = 0x0000FF00;
397+
bi.bV5BlueMask = 0x000000FF;
398+
bi.bV5AlphaMask = 0xFF000000;
399+
400+
void* bits = nullptr;
401+
HDC dc = GetDC(nullptr);
402+
HBITMAP color = CreateDIBSection(dc, reinterpret_cast<BITMAPINFO*>(&bi), DIB_RGB_COLORS, &bits, nullptr, 0);
403+
ReleaseDC(nullptr, dc);
404+
if(color==nullptr)
405+
return;
406+
407+
auto src = reinterpret_cast<const uint8_t*>(image.data());
408+
auto dst = reinterpret_cast<uint8_t*>(bits);
409+
for(size_t i=0; i<size_t(w*h); ++i) {
410+
dst[i*4+0] = src[i*4+2];
411+
dst[i*4+1] = src[i*4+1];
412+
dst[i*4+2] = src[i*4+0];
413+
dst[i*4+3] = src[i*4+3];
414+
}
415+
416+
// A 32-bit color bitmap with alpha takes precedence over the mask, which only needs to exist.
417+
HBITMAP mask = CreateBitmap(w, h, 1, 1, nullptr);
418+
419+
ICONINFO info = {};
420+
info.fIcon = FALSE;
421+
info.xHotspot = DWORD(hotX);
422+
info.yHotspot = DWORD(hotY);
423+
info.hbmMask = mask;
424+
info.hbmColor = color;
425+
customCursor = CreateIconIndirect(&info);
426+
427+
DeleteObject(color);
428+
DeleteObject(mask);
429+
}
430+
367431
long long WindowsApi::windowProc(void *_hWnd, uint32_t msg, const unsigned long long wParam, const long long lParam) {
368432
HWND hWnd = HWND(_hWnd);
369433

‎Engine/system/api/windowsapi.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class WindowsApi final : SystemApi {
2323

2424
void implSetCursorPosition(SystemApi::Window *w, int x, int y) override;
2525
void implShowCursor(SystemApi::Window *w, CursorShape show) override;
26+
void implSetCustomCursor(const Pixmap& image, int hotX, int hotY) override;
2627

2728
bool implIsRunning() override;
2829
int implExec(AppCallBack& cb) override;

‎Engine/system/api/x11api.cpp‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#ifdef __UNIX__
44
#include <Tempest/Event>
5+
#include <Tempest/Pixmap>
56
#include <Tempest/TextCodec>
67
#include <Tempest/Window>
78
#include <Tempest/Log>
@@ -455,10 +456,15 @@ void X11Api::implSetCursorPosition(SystemApi::Window *w, int x, int y) {
455456
}
456457
}
457458

459+
static Cursor customCursor = 0;
460+
458461
void X11Api::implShowCursor(SystemApi::Window *w, CursorShape show) {
459462
HWND hwnd(w);
460463

461-
if(show==CursorShape::Hidden) {
464+
if(show==CursorShape::Custom && customCursor!=0) {
465+
XDefineCursor(dpy, hwnd, customCursor);
466+
}
467+
else if(show==CursorShape::Hidden) {
462468
Cursor invisibleCursor;
463469
::Pixmap bitmapNoData;
464470
XColor black;
@@ -476,6 +482,7 @@ void X11Api::implShowCursor(SystemApi::Window *w, CursorShape show) {
476482
switch(show) {
477483
case CursorShape::Arrow:
478484
case CursorShape::Hidden:
485+
case CursorShape::Custom:
479486
break;
480487
case CursorShape::IBeam:
481488
name = "xterm";
@@ -508,6 +515,33 @@ void X11Api::implShowCursor(SystemApi::Window *w, CursorShape show) {
508515
XSync(dpy, False);
509516
}
510517

518+
void X11Api::implSetCustomCursor(const Tempest::Pixmap& image, int hotX, int hotY) {
519+
if(customCursor!=0) {
520+
XFreeCursor(dpy, customCursor);
521+
customCursor = 0;
522+
}
523+
if(image.isEmpty() || image.format()!=TextureFormat::RGBA8)
524+
return;
525+
526+
XcursorImage* img = XcursorImageCreate(int(image.w()), int(image.h()));
527+
if(img==nullptr)
528+
return;
529+
img->xhot = XcursorDim(hotX);
530+
img->yhot = XcursorDim(hotY);
531+
532+
// Xcursor expects premultiplied ARGB
533+
auto src = reinterpret_cast<const uint8_t*>(image.data());
534+
for(size_t i=0; i<size_t(image.w())*image.h(); ++i) {
535+
const uint32_t a = src[i*4+3];
536+
const uint32_t r = src[i*4+0]*a/255;
537+
const uint32_t g = src[i*4+1]*a/255;
538+
const uint32_t b = src[i*4+2]*a/255;
539+
img->pixels[i] = (a<<24) | (r<<16) | (g<<8) | b;
540+
}
541+
customCursor = XcursorImageLoadCursor(dpy, img);
542+
XcursorImageDestroy(img);
543+
}
544+
511545
Rect X11Api::implWindowClientRect(SystemApi::Window *w) {
512546
XWindowAttributes xwa;
513547
XGetWindowAttributes(dpy, HWND(w), &xwa);

‎Engine/system/api/x11api.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class X11Api final: SystemApi {
2929

3030
void implSetCursorPosition(SystemApi::Window *w, int x, int y) override;
3131
void implShowCursor(SystemApi::Window *w, CursorShape show) override;
32+
void implSetCustomCursor(const Tempest::Pixmap& image, int hotX, int hotY) override;
3233

3334
int implExec(AppCallBack& cb) override;
3435
void implProcessEvents(AppCallBack& cb) override;

‎Engine/system/systemapi.cpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ SystemApi::Data SystemApi::m;
2424
SystemApi::SystemApi() {
2525
}
2626

27+
void SystemApi::implSetCustomCursor(const Pixmap&, int, int) {
28+
// not implemented on this platform: CursorShape::Custom shows the arrow
29+
}
30+
2731
float SystemApi::implUiScale(Window* w) {
2832
// default to 1.0, on platforms without hidpi implementation
2933
return 1;
@@ -219,6 +223,10 @@ void SystemApi::showCursor(SystemApi::Window *w, CursorShape show) {
219223
return inst().implShowCursor(w,show);
220224
}
221225

226+
void SystemApi::setCustomCursor(const Pixmap& image, int hotX, int hotY) {
227+
return inst().implSetCustomCursor(image,hotX,hotY);
228+
}
229+
222230
CursorShape SystemApi::cursorShape(Tempest::Window& cb) {
223231
return cb.resolvedCursor;
224232
}

‎Engine/system/systemapi.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
namespace Tempest {
1212

13+
class Pixmap;
14+
1315
class SizeEvent;
1416
class DisplayMetricsEvent;
1517
class MouseEvent;
@@ -64,6 +66,7 @@ class SystemApi {
6466
static void setWindowTitle(SystemApi::Window *w, const char* utf8);
6567
static void setAllowedOrientations(SystemApi::Window *w, Orientation orientations);
6668
static float uiScale(SystemApi::Window *w);
69+
static void setCustomCursor(const Pixmap& image, int hotX, int hotY);
6770

6871
static uint16_t translateKey(uint64_t scancode);
6972
static void setupKeyTranslate(const TranslateKeyPair k[], uint16_t funcCount);
@@ -91,6 +94,7 @@ class SystemApi {
9194

9295
virtual void implSetCursorPosition(SystemApi::Window *w, int x, int y) = 0;
9396
virtual void implShowCursor(SystemApi::Window *w, CursorShape show) = 0;
97+
virtual void implSetCustomCursor(const Pixmap& image, int hotX, int hotY);
9498

9599
virtual float implUiScale(SystemApi::Window* w);
96100

‎Engine/ui/widgetstate.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace Tempest {
1212
SizeBDiag,
1313
SizeFDiag,
1414
SizeAll,
15+
Custom,
1516
};
1617

1718
class WidgetState final {

0 commit comments

Comments
 (0)