From e2cd18dba4d807b050c9e3e67fef658cb7a062ab Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 25 Aug 2026 23:24:20 +0800 Subject: [PATCH] [fix] detect macOS remote cursor shapes --- src/gui/runtime/cursor_state_provider_mac.mm | 147 +++++++++++++++---- 1 file changed, 115 insertions(+), 32 deletions(-) diff --git a/src/gui/runtime/cursor_state_provider_mac.mm b/src/gui/runtime/cursor_state_provider_mac.mm index 1e61c1b..465183f 100644 --- a/src/gui/runtime/cursor_state_provider_mac.mm +++ b/src/gui/runtime/cursor_state_provider_mac.mm @@ -5,42 +5,126 @@ #import #import +#include +#include + 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* cursors, NSCursor* cursor, + CursorShape shape) { + CursorFingerprint fingerprint; + if (FingerprintCursor(cursor, &fingerprint)) { + cursors->push_back({fingerprint, shape}); + } +} + +std::vector BuildKnownCursors() { + std::vector 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 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; }