Skip to content

Commit 3f48e2b

Browse files
committed
Add custom image cursors
1 parent b23ea2f commit 3f48e2b

9 files changed

Lines changed: 184 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: 47 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,46 @@ - (void)dispatchRenderer{
439446
::implShowCursor(w, show);
440447
}
441448

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

‎Engine/system/api/windowsapi.cpp‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
#ifdef __WINDOWS__
44

55
#include <Tempest/Event>
6+
#include <Tempest/Pixmap>
67
#include <Tempest/Except>
78
#include <Tempest/TextCodec>
89
#include <Tempest/Window>
910

1011
#include <atomic>
1112
#include <unordered_set>
1213
#include <thread>
14+
#include <vector>
1315

1416
#include <windows.h>
1517
#include <shellscalingapi.h>
@@ -328,7 +330,13 @@ void WindowsApi::implSetCursorPosition(SystemApi::Window *w, int x, int y) {
328330
}
329331
}
330332

333+
static HCURSOR customCursor = nullptr;
334+
331335
static void SetCursor(CursorShape show) {
336+
if(show==CursorShape::Custom && customCursor!=nullptr) {
337+
SetCursor(customCursor);
338+
return;
339+
}
332340
LPSTR idc = IDC_ARROW;
333341
switch(show) {
334342
case Tempest::CursorShape::Arrow:
@@ -355,6 +363,9 @@ static void SetCursor(CursorShape show) {
355363
case Tempest::CursorShape::SizeAll:
356364
idc = IDC_SIZEALL;
357365
break;
366+
case Tempest::CursorShape::Custom:
367+
idc = IDC_ARROW;
368+
break;
358369
}
359370
HCURSOR hcur = idc==nullptr ? nullptr : LoadCursorA(nullptr, idc);
360371
SetCursor(hcur);
@@ -364,6 +375,69 @@ void WindowsApi::implShowCursor(SystemApi::Window*, CursorShape show) {
364375
SetCursor(show);
365376
}
366377

378+
static HCURSOR makeCursor(const Pixmap& image, int hotX, int hotY) {
379+
if(image.isEmpty() || image.format()!=TextureFormat::RGBA8)
380+
return nullptr;
381+
382+
const int w = int(image.w());
383+
const int h = int(image.h());
384+
385+
BITMAPV5HEADER bi = {};
386+
bi.bV5Size = sizeof(bi);
387+
bi.bV5Width = w;
388+
bi.bV5Height = -h; // top-down rows, as in Pixmap
389+
bi.bV5Planes = 1;
390+
bi.bV5BitCount = 32;
391+
bi.bV5Compression = BI_BITFIELDS;
392+
bi.bV5RedMask = 0x00FF0000;
393+
bi.bV5GreenMask = 0x0000FF00;
394+
bi.bV5BlueMask = 0x000000FF;
395+
bi.bV5AlphaMask = 0xFF000000;
396+
397+
void* bits = nullptr;
398+
HDC dc = GetDC(nullptr);
399+
HBITMAP color = CreateDIBSection(dc, reinterpret_cast<BITMAPINFO*>(&bi), DIB_RGB_COLORS, &bits, nullptr, 0);
400+
ReleaseDC(nullptr, dc);
401+
if(color==nullptr)
402+
return nullptr;
403+
404+
auto src = reinterpret_cast<const uint8_t*>(image.data());
405+
auto dst = reinterpret_cast<uint8_t*>(bits);
406+
for(size_t i=0; i<size_t(w)*size_t(h); ++i) {
407+
dst[i*4+0] = src[i*4+2];
408+
dst[i*4+1] = src[i*4+1];
409+
dst[i*4+2] = src[i*4+0];
410+
dst[i*4+3] = src[i*4+3];
411+
}
412+
413+
// The alpha channel of a 32-bit color bitmap takes precedence; an empty AND mask keeps every pixel.
414+
std::vector<uint8_t> maskBits(size_t((w+15)/16*2)*size_t(h), 0);
415+
HBITMAP mask = CreateBitmap(w, h, 1, 1, maskBits.data());
416+
417+
ICONINFO info = {};
418+
info.fIcon = FALSE;
419+
info.xHotspot = DWORD(hotX);
420+
info.yHotspot = DWORD(hotY);
421+
info.hbmMask = mask;
422+
info.hbmColor = color;
423+
HCURSOR ret = CreateIconIndirect(&info);
424+
425+
DeleteObject(color);
426+
DeleteObject(mask);
427+
return ret;
428+
}
429+
430+
void WindowsApi::implSetCustomCursor(const Pixmap& image, int hotX, int hotY) {
431+
HCURSOR prev = customCursor;
432+
customCursor = makeCursor(image, hotX, hotY);
433+
if(prev==nullptr)
434+
return;
435+
// Swap an image that is on screen, so a destroyed cursor is never shown.
436+
if(GetCursor()==prev)
437+
SetCursor(customCursor!=nullptr ? customCursor : LoadCursorA(nullptr, IDC_ARROW));
438+
DestroyCursor(prev);
439+
}
440+
367441
long long WindowsApi::windowProc(void *_hWnd, uint32_t msg, const unsigned long long wParam, const long long lParam) {
368442
HWND hWnd = HWND(_hWnd);
369443

‎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: 40 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,38 @@ 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+
// Cursors are defined per window, so windows showing the old image get the new one
545+
for(auto& i:impl->windows)
546+
if(i.second.owner!=nullptr && SystemApi::cursorShape(*i.second.owner)==CursorShape::Custom)
547+
implShowCursor(i.first, CursorShape::Custom);
548+
}
549+
511550
Rect X11Api::implWindowClientRect(SystemApi::Window *w) {
512551
XWindowAttributes xwa;
513552
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
#include "eventdispatcher.h"
99

1010
#include <Tempest/Event>
11+
#include <Tempest/Pixmap>
1112
#include <Tempest/Window>
1213

14+
#include <algorithm>
15+
1316
using namespace Tempest;
1417

1518
static EventDispatcher dispatcher;
@@ -24,6 +27,10 @@ SystemApi::Data SystemApi::m;
2427
SystemApi::SystemApi() {
2528
}
2629

30+
void SystemApi::implSetCustomCursor(const Pixmap&, int, int) {
31+
// not implemented on this platform: CursorShape::Custom shows the arrow
32+
}
33+
2734
float SystemApi::implUiScale(Window* w) {
2835
// default to 1.0, on platforms without hidpi implementation
2936
return 1;
@@ -219,6 +226,14 @@ void SystemApi::showCursor(SystemApi::Window *w, CursorShape show) {
219226
return inst().implShowCursor(w,show);
220227
}
221228

229+
void SystemApi::setCustomCursor(const Pixmap& image, int hotX, int hotY) {
230+
if(!image.isEmpty()) {
231+
hotX = std::clamp(hotX, 0, int(image.w())-1);
232+
hotY = std::clamp(hotY, 0, int(image.h())-1);
233+
}
234+
return inst().implSetCustomCursor(image,hotX,hotY);
235+
}
236+
222237
CursorShape SystemApi::cursorShape(Tempest::Window& cb) {
223238
return cb.resolvedCursor;
224239
}

‎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)