[fix] detect macOS remote cursor shapes

This commit is contained in:
dijunkun
2026-08-25 23:24:20 +08:00
parent bd73ef55f1
commit e2cd18dba4
+115 -32
View File
@@ -5,42 +5,126 @@
#import <AppKit/AppKit.h>
#import <CoreGraphics/CGRemoteOperation.h>
#include <cstdint>
#include <vector>
namespace crossdesk {
namespace {
bool SameCursor(NSCursor* left, NSCursor* right) {
if (left == right || [left isEqual:right]) return true;
return left && right && NSEqualPoints(left.hotSpot, right.hotSpot) &&
[left.image isEqual:right.image];
struct CursorFingerprint {
uint64_t pixel_hash = 0;
size_t width = 0;
size_t height = 0;
size_t bytes_per_row = 0;
CFIndex pixel_bytes = 0;
NSPoint hot_spot = NSZeroPoint;
};
struct KnownCursor {
CursorFingerprint fingerprint;
CursorShape shape = CursorShape::default_cursor;
};
bool FingerprintCursor(NSCursor* cursor, CursorFingerprint* fingerprint) {
if (!cursor || !fingerprint || !cursor.image) return false;
NSRect proposed_rect = {NSZeroPoint, cursor.image.size};
CGImageRef image =
[cursor.image CGImageForProposedRect:&proposed_rect
context:nil
hints:nil];
if (!image) return false;
CGDataProviderRef provider = CGImageGetDataProvider(image);
if (!provider) return false;
CFDataRef data = CGDataProviderCopyData(provider);
if (!data) return false;
// NSCursor.currentSystemCursor returns a fresh NSCursor/NSImage wrapper on
// each query. Hash the decoded CGImage pixels so it can still be matched to
// AppKit's semantic cursor instances without encoding a large TIFF every
// frame.
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ULL;
constexpr uint64_t kFnvPrime = 1099511628211ULL;
uint64_t hash = kFnvOffsetBasis;
const UInt8* bytes = CFDataGetBytePtr(data);
const CFIndex length = CFDataGetLength(data);
for (CFIndex index = 0; index < length; ++index) {
hash ^= bytes[index];
hash *= kFnvPrime;
}
fingerprint->pixel_hash = hash;
fingerprint->width = CGImageGetWidth(image);
fingerprint->height = CGImageGetHeight(image);
fingerprint->bytes_per_row = CGImageGetBytesPerRow(image);
fingerprint->pixel_bytes = length;
fingerprint->hot_spot = cursor.hotSpot;
CFRelease(data);
return true;
}
bool SameCursor(const CursorFingerprint& left,
const CursorFingerprint& right) {
return left.pixel_hash == right.pixel_hash && left.width == right.width &&
left.height == right.height &&
left.bytes_per_row == right.bytes_per_row &&
left.pixel_bytes == right.pixel_bytes &&
NSEqualPoints(left.hot_spot, right.hot_spot);
}
void AddKnownCursor(std::vector<KnownCursor>* cursors, NSCursor* cursor,
CursorShape shape) {
CursorFingerprint fingerprint;
if (FingerprintCursor(cursor, &fingerprint)) {
cursors->push_back({fingerprint, shape});
}
}
std::vector<KnownCursor> BuildKnownCursors() {
std::vector<KnownCursor> cursors;
cursors.reserve(16);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
AddKnownCursor(&cursors, NSCursor.arrowCursor, CursorShape::default_cursor);
AddKnownCursor(&cursors, NSCursor.pointingHandCursor, CursorShape::pointer);
AddKnownCursor(&cursors, NSCursor.crosshairCursor, CursorShape::crosshair);
AddKnownCursor(&cursors, NSCursor.IBeamCursor, CursorShape::text);
AddKnownCursor(&cursors, NSCursor.IBeamCursorForVerticalLayout,
CursorShape::text);
AddKnownCursor(&cursors, NSCursor.operationNotAllowedCursor,
CursorShape::not_allowed);
AddKnownCursor(&cursors, NSCursor.dragLinkCursor, CursorShape::alias);
AddKnownCursor(&cursors, NSCursor.dragCopyCursor, CursorShape::copy);
AddKnownCursor(&cursors, NSCursor.openHandCursor, CursorShape::grab);
AddKnownCursor(&cursors, NSCursor.closedHandCursor, CursorShape::grabbing);
AddKnownCursor(&cursors, NSCursor.resizeLeftRightCursor,
CursorShape::ew_resize);
AddKnownCursor(&cursors, NSCursor.resizeUpDownCursor, CursorShape::ns_resize);
AddKnownCursor(&cursors, NSCursor.resizeUpCursor, CursorShape::n_resize);
AddKnownCursor(&cursors, NSCursor.resizeRightCursor, CursorShape::e_resize);
AddKnownCursor(&cursors, NSCursor.resizeDownCursor, CursorShape::s_resize);
AddKnownCursor(&cursors, NSCursor.resizeLeftCursor, CursorShape::w_resize);
#pragma clang diagnostic pop
return cursors;
}
CursorShape ShapeFromMacCursor(NSCursor* cursor) {
if (SameCursor(cursor, NSCursor.pointingHandCursor))
return CursorShape::pointer;
if (SameCursor(cursor, NSCursor.crosshairCursor))
return CursorShape::crosshair;
if (SameCursor(cursor, NSCursor.IBeamCursor) ||
SameCursor(cursor, NSCursor.IBeamCursorForVerticalLayout))
return CursorShape::text;
if (SameCursor(cursor, NSCursor.operationNotAllowedCursor))
return CursorShape::not_allowed;
if (SameCursor(cursor, NSCursor.dragLinkCursor)) return CursorShape::alias;
if (SameCursor(cursor, NSCursor.dragCopyCursor)) return CursorShape::copy;
if (SameCursor(cursor, NSCursor.openHandCursor)) return CursorShape::grab;
if (SameCursor(cursor, NSCursor.closedHandCursor))
return CursorShape::grabbing;
if (SameCursor(cursor, NSCursor.resizeLeftRightCursor))
return CursorShape::ew_resize;
if (SameCursor(cursor, NSCursor.resizeUpDownCursor))
return CursorShape::ns_resize;
if (SameCursor(cursor, NSCursor.resizeUpCursor))
return CursorShape::n_resize;
if (SameCursor(cursor, NSCursor.resizeRightCursor))
return CursorShape::e_resize;
if (SameCursor(cursor, NSCursor.resizeDownCursor))
return CursorShape::s_resize;
if (SameCursor(cursor, NSCursor.resizeLeftCursor))
return CursorShape::w_resize;
CursorFingerprint fingerprint;
if (!FingerprintCursor(cursor, &fingerprint)) {
return CursorShape::default_cursor;
}
// Sampling begins from the UI tick, after AppKit has initialized its cursor
// catalog. Cache those fingerprints because only the current cursor needs
// to be decoded on subsequent frames.
static const std::vector<KnownCursor> known_cursors = BuildKnownCursors();
for (const KnownCursor& known : known_cursors) {
if (SameCursor(fingerprint, known.fingerprint)) return known.shape;
}
return CursorShape::default_cursor;
}
@@ -62,8 +146,7 @@ bool CursorStateProvider::Sample(CursorState* state) {
state->seq = 0;
state->visible = visible && cursor != nil;
state->shape = state->visible ? ShapeFromMacCursor(cursor)
: CursorShape::none;
state->shape = state->visible ? ShapeFromMacCursor(cursor) : CursorShape::none;
return true;
}