node-mac-recorder 2.20.2 → 2.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cursor_tracker.mm +317 -320
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.20.2",
3
+ "version": "2.20.4",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -7,6 +7,10 @@
7
7
  #import <Accessibility/Accessibility.h>
8
8
  #import <dispatch/dispatch.h>
9
9
 
10
+ #ifndef kAXHitTestParameterizedAttribute
11
+ #define kAXHitTestParameterizedAttribute CFSTR("AXHitTest")
12
+ #endif
13
+
10
14
  // Global state for cursor tracking
11
15
  static bool g_isCursorTracking = false;
12
16
  static CFMachPortRef g_eventTap = NULL;
@@ -49,309 +53,330 @@ static NSString* CopyAndReleaseCFString(CFStringRef value) {
49
53
  return result;
50
54
  }
51
55
 
52
- // Mouse button state tracking
53
- static bool g_leftMouseDown = false;
54
- static bool g_rightMouseDown = false;
55
- static NSString *g_lastEventType = @"move";
56
+ static inline BOOL StringEqualsAny(NSString *value, NSArray<NSString *> *candidates) {
57
+ if (!value) {
58
+ return NO;
59
+ }
60
+ for (NSString *candidate in candidates) {
61
+ if ([value isEqualToString:candidate]) {
62
+ return YES;
63
+ }
64
+ }
65
+ return NO;
66
+ }
67
+
68
+ static NSString* CopyAttributeString(AXUIElementRef element, CFStringRef attribute) {
69
+ if (!element || !attribute) {
70
+ return nil;
71
+ }
56
72
 
57
- // Event tap callback
58
- static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) {
59
- return event;
73
+ CFStringRef value = NULL;
74
+ AXError error = AXUIElementCopyAttributeValue(element, attribute, (CFTypeRef *)&value);
75
+ if (error == kAXErrorSuccess && value) {
76
+ return CopyAndReleaseCFString(value);
77
+ }
78
+
79
+ if (value) {
80
+ CFRelease(value);
81
+ }
82
+ return nil;
60
83
  }
61
84
 
62
- // Accessibility tabanlı cursor tip tespiti
63
- static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
64
- @autoreleasepool {
65
- @try {
66
- // ACCESSIBILITY API BASED CURSOR DETECTION
67
- // Determine cursor type based on the UI element under the cursor
85
+ static BOOL CopyAttributeBoolean(AXUIElementRef element, CFStringRef attribute, BOOL *outValue) {
86
+ if (!element || !attribute || !outValue) {
87
+ return NO;
88
+ }
68
89
 
69
- AXUIElementRef systemWide = AXUIElementCreateSystemWide();
70
- AXUIElementRef elementAtPosition = NULL;
71
- AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
90
+ CFTypeRef rawValue = NULL;
91
+ AXError error = AXUIElementCopyAttributeValue(element, attribute, &rawValue);
92
+ if (error != kAXErrorSuccess || !rawValue) {
93
+ if (rawValue) {
94
+ CFRelease(rawValue);
95
+ }
96
+ return NO;
97
+ }
72
98
 
73
- NSString *cursorType = @"default"; // Default fallback
99
+ BOOL result = NO;
100
+ if (CFGetTypeID(rawValue) == CFBooleanGetTypeID()) {
101
+ result = CFBooleanGetValue((CFBooleanRef)rawValue);
102
+ }
74
103
 
75
- if (error == kAXErrorSuccess && elementAtPosition) {
76
- NSString *elementRole = nil;
77
- CFStringRef role = NULL;
78
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
79
- if (error == kAXErrorSuccess && role) {
80
- elementRole = CopyAndReleaseCFString(role);
81
- }
104
+ CFRelease(rawValue);
105
+ *outValue = result;
106
+ return YES;
107
+ }
82
108
 
83
- if (elementRole) {
84
- NSLog(@"🎯 ELEMENT ROLE: %@", elementRole);
85
- }
109
+ static BOOL ElementHasAction(AXUIElementRef element, CFStringRef action) {
110
+ if (!element || !action) {
111
+ return NO;
112
+ }
86
113
 
87
- // TEXT CURSORS
88
- if ([elementRole isEqualToString:@"AXTextField"] ||
89
- [elementRole isEqualToString:@"AXTextArea"] ||
90
- [elementRole isEqualToString:@"AXStaticText"] ||
91
- [elementRole isEqualToString:@"AXSearchField"]) {
92
- cursorType = @"text";
93
- }
94
- // POINTER CURSORS (clickable elements)
95
- else if ([elementRole isEqualToString:@"AXLink"] ||
96
- [elementRole isEqualToString:@"AXButton"] ||
97
- [elementRole isEqualToString:@"AXMenuItem"] ||
98
- [elementRole isEqualToString:@"AXRadioButton"] ||
99
- [elementRole isEqualToString:@"AXCheckBox"] ||
100
- [elementRole isEqualToString:@"AXPopUpButton"] ||
101
- [elementRole isEqualToString:@"AXTab"]) {
102
- cursorType = @"pointer";
103
- }
104
- // GRAB CURSORS (draggable elements)
105
- else if ([elementRole isEqualToString:@"AXImage"] ||
106
- [elementRole isEqualToString:@"AXGroup"]) {
107
- // Check if element is draggable
108
- CFBooleanRef draggable = NULL;
109
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXMovable"), (CFTypeRef*)&draggable);
110
- if (error == kAXErrorSuccess && draggable && CFBooleanGetValue(draggable)) {
111
- cursorType = @"grab";
112
- } else {
113
- cursorType = @"default";
114
- }
115
- }
116
- // PROGRESS CURSORS (loading/busy elements)
117
- else if ([elementRole isEqualToString:@"AXProgressIndicator"] ||
118
- [elementRole isEqualToString:@"AXBusyIndicator"]) {
119
- cursorType = @"progress";
120
- }
121
- // HELP CURSORS (help buttons/tooltips)
122
- else if ([elementRole isEqualToString:@"AXHelpTag"] ||
123
- [elementRole isEqualToString:@"AXTooltip"]) {
124
- cursorType = @"help";
125
- }
126
- // RESIZE CURSORS - sadece AXSplitter için
127
- else if ([elementRole isEqualToString:@"AXSplitter"]) {
128
- // Get splitter orientation to determine resize direction
129
- CFStringRef orientation = NULL;
130
- error = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXOrientation"), (CFTypeRef*)&orientation);
131
- if (error == kAXErrorSuccess && orientation) {
132
- NSString *orientationStr = CopyAndReleaseCFString(orientation);
133
- if ([orientationStr isEqualToString:@"AXHorizontalOrientation"]) {
134
- cursorType = @"ns-resize"; // Yatay splitter -> dikey hareket (north-south)
135
- } else if ([orientationStr isEqualToString:@"AXVerticalOrientation"]) {
136
- cursorType = @"col-resize"; // Dikey splitter -> yatay hareket (east-west)
137
- } else {
138
- cursorType = @"default"; // Bilinmeyen orientation
139
- }
140
- } else {
141
- cursorType = @"default"; // Orientation alınamazsa default
142
- }
143
- }
144
- // SCROLL CURSORS - hep default olsun, all-scroll görünmesin
145
- else if ([elementRole isEqualToString:@"AXScrollBar"]) {
146
- cursorType = @"default"; // ScrollBar'lar için de default
147
- }
148
- // AXScrollArea - hep default
149
- else if ([elementRole isEqualToString:@"AXScrollArea"]) {
150
- cursorType = @"default"; // ScrollArea her zaman default
151
- }
152
- // CROSSHAIR CURSORS (drawing/selection tools)
153
- else if ([elementRole isEqualToString:@"AXCanvas"] ||
154
- [elementRole isEqualToString:@"AXDrawingArea"]) {
155
- cursorType = @"crosshair";
156
- }
157
- // ZOOM CURSORS (zoom controls)
158
- else if ([elementRole isEqualToString:@"AXZoomButton"]) {
159
- cursorType = @"zoom-in";
160
- }
161
- // NOT-ALLOWED CURSORS (disabled elements)
162
- else if ([elementRole isEqualToString:@"AXStaticText"] ||
163
- [elementRole isEqualToString:@"AXGroup"]) {
164
- // Check if element is disabled/readonly
165
- CFBooleanRef enabled = NULL;
166
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXEnabledAttribute, (CFTypeRef*)&enabled);
167
- if (error == kAXErrorSuccess && enabled && !CFBooleanGetValue(enabled)) {
168
- cursorType = @"not-allowed";
169
- }
170
- }
171
- // WINDOW BORDER RESIZE - sadece pencere kenarlarında
172
- else if ([elementRole isEqualToString:@"AXWindow"]) {
173
- // Check window attributes to see if it's resizable
174
- CFBooleanRef resizable = NULL;
175
- AXError resizableError = AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXResizeButton"), (CFTypeRef*)&resizable);
176
-
177
- // Sadece resize edilebilir pencereler için cursor değişimi
178
- if (resizableError == kAXErrorSuccess || true) { // AXResizeButton bulunamazsa da devam et
179
- CFTypeRef position = NULL;
180
- CFTypeRef size = NULL;
181
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXPositionAttribute, &position);
182
- AXError sizeError = AXUIElementCopyAttributeValue(elementAtPosition, kAXSizeAttribute, &size);
183
-
184
- if (error == kAXErrorSuccess && sizeError == kAXErrorSuccess && position && size) {
185
- CGPoint windowPos;
186
- CGSize windowSize;
187
- AXValueGetValue((AXValueRef)position, kAXValueTypeCGPoint, &windowPos);
188
- AXValueGetValue((AXValueRef)size, kAXValueTypeCGSize, &windowSize);
189
-
190
- CGFloat x = cursorPos.x - windowPos.x;
191
- CGFloat y = cursorPos.y - windowPos.y;
192
- CGFloat w = windowSize.width;
193
- CGFloat h = windowSize.height;
194
- CGFloat edge = 3.0; // Daha küçük edge detection (3px)
195
-
196
- // Sadece çok kenar köşelerde resize cursor'ı göster
197
- BOOL isOnBorder = NO;
198
-
199
- // Corner resize detection - çok dar alanda, doğru açılar
200
- if (x <= edge && y <= edge) {
201
- cursorType = @"nwse-resize"; // Sol üst köşe - northwest-southeast
202
- isOnBorder = YES;
203
- }
204
- else if (x >= w-edge && y <= edge) {
205
- cursorType = @"nesw-resize"; // Sağ üst köşe - northeast-southwest
206
- isOnBorder = YES;
207
- }
208
- else if (x <= edge && y >= h-edge) {
209
- cursorType = @"nesw-resize"; // Sol alt köşe - southwest-northeast
210
- isOnBorder = YES;
211
- }
212
- else if (x >= w-edge && y >= h-edge) {
213
- cursorType = @"nwse-resize"; // Sağ alt köşe - southeast-northwest
214
- isOnBorder = YES;
215
- }
216
- // Edge resize detection - sadece çok kenarlarda
217
- else if (x <= edge && y > edge && y < h-edge) {
218
- cursorType = @"col-resize"; // Sol kenar - column resize (yatay)
219
- isOnBorder = YES;
220
- }
221
- else if (x >= w-edge && y > edge && y < h-edge) {
222
- cursorType = @"col-resize"; // Sağ kenar - column resize (yatay)
223
- isOnBorder = YES;
224
- }
225
- else if (y <= edge && x > edge && x < w-edge) {
226
- cursorType = @"ns-resize"; // Üst kenar - north-south resize (dikey)
227
- isOnBorder = YES;
228
- }
229
- else if (y >= h-edge && x > edge && x < w-edge) {
230
- cursorType = @"ns-resize"; // Alt kenar - north-south resize (dikey)
231
- isOnBorder = YES;
232
- }
233
-
234
- // Eğer border'da değilse default
235
- if (!isOnBorder) {
236
- cursorType = @"default";
237
- }
238
-
239
- if (position) CFRelease(position);
240
- if (size) CFRelease(size);
241
- } else {
242
- cursorType = @"default";
243
- }
244
- } else {
245
- cursorType = @"default";
246
- }
247
- }
248
- // HER DURUM İÇİN DEFAULT FALLBACK
249
- else {
250
- // Bilinmeyen elementler için her zaman default
251
- cursorType = @"default";
252
- }
114
+ CFArrayRef actions = NULL;
115
+ AXError error = AXUIElementCopyActionNames(element, &actions);
116
+ if (error != kAXErrorSuccess || !actions) {
117
+ return NO;
118
+ }
253
119
 
254
- // Check subroles for additional context
255
- CFStringRef subrole = NULL;
256
- error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
257
- if (error == kAXErrorSuccess && subrole) {
258
- NSString *elementSubrole = CopyAndReleaseCFString(subrole);
259
- NSLog(@"🎯 ELEMENT SUBROLE: %@", elementSubrole);
260
-
261
- // Subrole override'ları - sadece çok spesifik durumlar için
262
- if ([elementSubrole isEqualToString:@"AXCloseButton"] ||
263
- [elementSubrole isEqualToString:@"AXMinimizeButton"] ||
264
- [elementSubrole isEqualToString:@"AXZoomButton"] ||
265
- [elementSubrole isEqualToString:@"AXToolbarButton"]) {
266
- cursorType = @"pointer";
267
- }
268
- // Copy/alias subroles - sadece bu durumlar için override
269
- else if ([elementSubrole isEqualToString:@"AXFileDrop"] ||
270
- [elementSubrole isEqualToString:@"AXDropTarget"]) {
271
- cursorType = @"copy";
272
- }
273
- // Alias/shortcut subroles
274
- else if ([elementSubrole isEqualToString:@"AXAlias"] ||
275
- [elementSubrole isEqualToString:@"AXShortcut"]) {
276
- cursorType = @"alias";
277
- }
278
- // Grabbing state (being dragged) - sadece gerçek drag sırasında
279
- else if ([elementSubrole isEqualToString:@"AXDragging"] ||
280
- [elementSubrole isEqualToString:@"AXMoving"]) {
281
- cursorType = @"grabbing";
282
- }
283
- // Zoom controls - sadece spesifik zoom butonları için
284
- else if ([elementSubrole isEqualToString:@"AXZoomIn"]) {
285
- cursorType = @"zoom-in";
286
- }
287
- else if ([elementSubrole isEqualToString:@"AXZoomOut"]) {
288
- cursorType = @"zoom-out";
289
- }
290
- // Subrole'dan bir şey bulamazsa role-based cursor'ı koruyoruz
291
- }
120
+ BOOL hasAction = NO;
121
+ CFIndex count = CFArrayGetCount(actions);
122
+ for (CFIndex i = 0; i < count; i++) {
123
+ CFStringRef candidate = (CFStringRef)CFArrayGetValueAtIndex(actions, i);
124
+ if (CFStringCompare(candidate, action, 0) == kCFCompareEqualTo) {
125
+ hasAction = YES;
126
+ break;
127
+ }
128
+ }
129
+ CFRelease(actions);
130
+ return hasAction;
131
+ }
292
132
 
293
- CFStringRef roleDescriptionRef = NULL;
294
- AXError descriptionError = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleDescriptionAttribute, (CFTypeRef*)&roleDescriptionRef);
295
- if (descriptionError == kAXErrorSuccess && roleDescriptionRef) {
296
- NSString *roleDescription = CopyAndReleaseCFString(roleDescriptionRef);
297
- if (roleDescription) {
298
- NSString *roleDescriptionLower = [roleDescription lowercaseString];
299
- if ([roleDescriptionLower containsString:@"text"] ||
300
- [roleDescriptionLower containsString:@"editor"] ||
301
- [roleDescriptionLower containsString:@"code"] ||
302
- [roleDescriptionLower containsString:@"document"]) {
303
- cursorType = @"text";
304
- } else if ([roleDescriptionLower containsString:@"button"] ||
305
- [roleDescriptionLower containsString:@"link"] ||
306
- [roleDescriptionLower containsString:@"tab"]) {
307
- cursorType = @"pointer";
308
- }
309
- }
310
- }
133
+ static BOOL PointInsideElementFrame(AXUIElementRef element, CGPoint point) {
134
+ if (!element) {
135
+ return NO;
136
+ }
311
137
 
312
- CFBooleanRef isEditable = NULL;
313
- if (AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXEditable"), (CFTypeRef*)&isEditable) == kAXErrorSuccess &&
314
- isEditable && CFBooleanGetValue(isEditable)) {
315
- cursorType = @"text";
316
- }
138
+ AXValueRef positionValue = NULL;
139
+ AXValueRef sizeValue = NULL;
317
140
 
318
- CFBooleanRef supportsTextSelection = NULL;
319
- if (AXUIElementCopyAttributeValue(elementAtPosition, CFSTR("AXSupportsTextSelection"), (CFTypeRef*)&supportsTextSelection) == kAXErrorSuccess &&
320
- supportsTextSelection && CFBooleanGetValue(supportsTextSelection)) {
321
- cursorType = @"text";
322
- }
141
+ AXError positionError = AXUIElementCopyAttributeValue(element, kAXPositionAttribute, (CFTypeRef *)&positionValue);
142
+ AXError sizeError = AXUIElementCopyAttributeValue(element, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
323
143
 
324
- CFTypeRef valueAttribute = NULL;
325
- if (AXUIElementCopyAttributeValue(elementAtPosition, kAXValueAttribute, &valueAttribute) == kAXErrorSuccess && valueAttribute) {
326
- CFTypeID typeId = CFGetTypeID(valueAttribute);
327
- if (typeId == CFAttributedStringGetTypeID() ||
328
- typeId == CFStringGetTypeID()) {
329
- cursorType = @"text";
330
- }
331
- CFRelease(valueAttribute);
332
- }
144
+ if (positionError != kAXErrorSuccess || sizeError != kAXErrorSuccess || !positionValue || !sizeValue) {
145
+ if (positionValue) CFRelease(positionValue);
146
+ if (sizeValue) CFRelease(sizeValue);
147
+ return NO;
148
+ }
333
149
 
334
- }
150
+ CGPoint origin = CGPointZero;
151
+ CGSize size = CGSizeZero;
152
+ AXValueGetValue(positionValue, kAXValueTypeCGPoint, &origin);
153
+ AXValueGetValue(sizeValue, kAXValueTypeCGSize, &size);
335
154
 
336
- if (elementAtPosition) {
337
- CFRelease(elementAtPosition);
338
- }
339
- if (systemWide) {
340
- CFRelease(systemWide);
341
- }
155
+ CFRelease(positionValue);
156
+ CFRelease(sizeValue);
157
+
158
+ CGRect frame = CGRectMake(origin.x, origin.y, size.width, size.height);
159
+ return CGRectContainsPoint(frame, point);
160
+ }
161
+
162
+ static NSString* CursorTypeForWindowBorder(AXUIElementRef element, CGPoint cursorPos) {
163
+ AXValueRef positionValue = NULL;
164
+ AXValueRef sizeValue = NULL;
165
+
166
+ AXError positionError = AXUIElementCopyAttributeValue(element, kAXPositionAttribute, (CFTypeRef *)&positionValue);
167
+ AXError sizeError = AXUIElementCopyAttributeValue(element, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
342
168
 
343
- // Son güvence - eğer cursorType hala nil veya geçersizse default'a çevir
344
- if (!cursorType || [cursorType length] == 0) {
345
- cursorType = @"default";
169
+ if (positionError != kAXErrorSuccess || sizeError != kAXErrorSuccess || !positionValue || !sizeValue) {
170
+ if (positionValue) CFRelease(positionValue);
171
+ if (sizeValue) CFRelease(sizeValue);
172
+ return nil;
173
+ }
174
+
175
+ CGPoint windowOrigin = CGPointZero;
176
+ CGSize windowSize = CGSizeZero;
177
+ AXValueGetValue(positionValue, kAXValueTypeCGPoint, &windowOrigin);
178
+ AXValueGetValue(sizeValue, kAXValueTypeCGSize, &windowSize);
179
+
180
+ CFRelease(positionValue);
181
+ CFRelease(sizeValue);
182
+
183
+ CGFloat edge = 4.0;
184
+ CGFloat x = cursorPos.x - windowOrigin.x;
185
+ CGFloat y = cursorPos.y - windowOrigin.y;
186
+ CGFloat w = windowSize.width;
187
+ CGFloat h = windowSize.height;
188
+
189
+ if (x < 0 || y < 0 || x > w || y > h) {
190
+ return nil;
191
+ }
192
+
193
+ BOOL nearLeft = (x >= 0 && x <= edge);
194
+ BOOL nearRight = (x >= w - edge && x <= w);
195
+ BOOL nearTop = (y >= 0 && y <= edge);
196
+ BOOL nearBottom = (y >= h - edge && y <= h);
197
+
198
+ if ((nearLeft && nearTop) || (nearRight && nearBottom)) {
199
+ return @"nwse-resize";
200
+ }
201
+ if ((nearRight && nearTop) || (nearLeft && nearBottom)) {
202
+ return @"nesw-resize";
203
+ }
204
+ if (nearLeft || nearRight) {
205
+ return @"col-resize";
206
+ }
207
+ if (nearTop || nearBottom) {
208
+ return @"ns-resize";
209
+ }
210
+
211
+ return nil;
212
+ }
213
+
214
+ static NSString* CursorTypeFromAccessibilityElement(AXUIElementRef element, CGPoint cursorPos) {
215
+ if (!element) {
216
+ return nil;
217
+ }
218
+
219
+ NSString *role = CopyAttributeString(element, kAXRoleAttribute);
220
+ NSString *subrole = CopyAttributeString(element, kAXSubroleAttribute);
221
+ NSString *roleDescription = CopyAttributeString(element, kAXRoleDescriptionAttribute);
222
+
223
+ if (StringEqualsAny(role, @[@"AXTextField", @"AXTextArea", @"AXSearchField"])) {
224
+ return @"text";
225
+ }
226
+
227
+ if (StringEqualsAny(subrole, @[@"AXSecureTextField", @"AXTextField"])) {
228
+ return @"text";
229
+ }
230
+
231
+ BOOL isEditable = NO;
232
+ if (CopyAttributeBoolean(element, CFSTR("AXEditable"), &isEditable) && isEditable) {
233
+ return @"text";
234
+ }
235
+
236
+ BOOL supportsSelection = NO;
237
+ if (CopyAttributeBoolean(element, CFSTR("AXSupportsTextSelection"), &supportsSelection) && supportsSelection) {
238
+ return @"text";
239
+ }
240
+
241
+ CFTypeRef valueAttribute = NULL;
242
+ if (AXUIElementCopyAttributeValue(element, kAXValueAttribute, &valueAttribute) == kAXErrorSuccess && valueAttribute) {
243
+ CFTypeID typeId = CFGetTypeID(valueAttribute);
244
+ if (typeId == CFAttributedStringGetTypeID() || typeId == CFStringGetTypeID()) {
245
+ CFRelease(valueAttribute);
246
+ return @"text";
247
+ }
248
+ CFRelease(valueAttribute);
249
+ }
250
+
251
+ if (StringEqualsAny(role, @[@"AXProgressIndicator", @"AXBusyIndicator"])) {
252
+ return @"progress";
253
+ }
254
+
255
+ if (StringEqualsAny(role, @[@"AXHelpTag", @"AXTooltip"])) {
256
+ return @"help";
257
+ }
258
+
259
+ if ([role isEqualToString:@"AXSplitter"]) {
260
+ NSString *orientation = CopyAttributeString(element, CFSTR("AXOrientation"));
261
+ if ([orientation isEqualToString:@"AXHorizontalOrientation"]) {
262
+ return @"ns-resize";
263
+ }
264
+ if ([orientation isEqualToString:@"AXVerticalOrientation"]) {
265
+ return @"col-resize";
266
+ }
267
+ }
268
+
269
+ if ([role isEqualToString:@"AXWindow"]) {
270
+ NSString *windowCursor = CursorTypeForWindowBorder(element, cursorPos);
271
+ if (windowCursor) {
272
+ return windowCursor;
273
+ }
274
+ }
275
+
276
+ if (StringEqualsAny(role, @[@"AXMenuItem", @"AXButton", @"AXLink", @"AXTab", @"AXRadioButton", @"AXCheckBox", @"AXPopUpButton"])) {
277
+ return @"pointer";
278
+ }
279
+
280
+ if (StringEqualsAny(subrole, @[@"AXLink", @"AXToolbarButton", @"AXFileDrop", @"AXDropTarget"])) {
281
+ return @"pointer";
282
+ }
283
+
284
+ if (roleDescription) {
285
+ NSString *lower = [roleDescription lowercaseString];
286
+ if ([lower containsString:@"button"] ||
287
+ [lower containsString:@"link"] ||
288
+ [lower containsString:@"tab"]) {
289
+ return @"pointer";
290
+ }
291
+ if ([lower containsString:@"text"] ||
292
+ [lower containsString:@"editor"] ||
293
+ [lower containsString:@"document"]) {
294
+ return @"text";
295
+ }
296
+ }
297
+
298
+ if (ElementHasAction(element, kAXPressAction) ||
299
+ ElementHasAction(element, kAXShowMenuAction) ||
300
+ ElementHasAction(element, CFSTR("AXConfirm"))) {
301
+ return @"pointer";
302
+ }
303
+
304
+ CFTypeRef urlValue = NULL;
305
+ if (AXUIElementCopyAttributeValue(element, kAXURLAttribute, &urlValue) == kAXErrorSuccess && urlValue) {
306
+ CFRelease(urlValue);
307
+ return @"pointer";
308
+ }
309
+ if (urlValue) {
310
+ CFRelease(urlValue);
311
+ }
312
+
313
+ BOOL isDraggable = NO;
314
+ if (CopyAttributeBoolean(element, CFSTR("AXDraggable"), &isDraggable) && isDraggable) {
315
+ return @"grab";
316
+ }
317
+
318
+ BOOL isMovable = NO;
319
+ if (CopyAttributeBoolean(element, CFSTR("AXMovable"), &isMovable) && isMovable) {
320
+ return @"grab";
321
+ }
322
+
323
+ if (subrole && [subrole isEqualToString:@"AXZoomIn"]) {
324
+ return @"zoom-in";
325
+ }
326
+ if (subrole && [subrole isEqualToString:@"AXZoomOut"]) {
327
+ return @"zoom-out";
328
+ }
329
+
330
+ return nil;
331
+ }
332
+
333
+ // Mouse button state tracking
334
+ static bool g_leftMouseDown = false;
335
+ static bool g_rightMouseDown = false;
336
+ static NSString *g_lastEventType = @"move";
337
+
338
+ // Accessibility tabanlı cursor tip tespiti
339
+ static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
340
+ @autoreleasepool {
341
+ AXUIElementRef systemWide = AXUIElementCreateSystemWide();
342
+ if (!systemWide) {
343
+ return nil;
344
+ }
345
+
346
+ NSString *cursorType = nil;
347
+
348
+ AXUIElementRef elementAtPosition = NULL;
349
+ AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
350
+ if (error == kAXErrorSuccess && elementAtPosition) {
351
+ cursorType = CursorTypeFromAccessibilityElement(elementAtPosition, cursorPos);
352
+ CFRelease(elementAtPosition);
353
+ }
354
+
355
+ if (!cursorType) {
356
+ AXValueRef pointValue = AXValueCreate(kAXValueTypeCGPoint, &cursorPos);
357
+ if (pointValue) {
358
+ AXUIElementRef hitElement = NULL;
359
+ AXError hitError = AXUIElementCopyParameterizedAttributeValue(systemWide, kAXHitTestParameterizedAttribute, pointValue, (CFTypeRef *)&hitElement);
360
+ CFRelease(pointValue);
361
+ if (hitError == kAXErrorSuccess && hitElement) {
362
+ cursorType = CursorTypeFromAccessibilityElement(hitElement, cursorPos);
363
+ CFRelease(hitElement);
364
+ }
346
365
  }
366
+ }
347
367
 
348
- NSLog(@"🎯 AX CURSOR TYPE: %@", cursorType);
349
- return cursorType;
350
-
351
- } @catch (NSException *exception) {
352
- NSLog(@"Error in detectCursorTypeUsingAccessibility: %@", exception);
353
- return @"default";
368
+ if (!cursorType) {
369
+ AXUIElementRef focusedElement = NULL;
370
+ if (AXUIElementCopyAttributeValue(systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement) == kAXErrorSuccess && focusedElement) {
371
+ if (PointInsideElementFrame(focusedElement, cursorPos)) {
372
+ cursorType = CursorTypeFromAccessibilityElement(focusedElement, cursorPos);
373
+ }
374
+ CFRelease(focusedElement);
375
+ }
354
376
  }
377
+
378
+ CFRelease(systemWide);
379
+ return cursorType;
355
380
  }
356
381
  }
357
382
 
@@ -569,12 +594,10 @@ NSString* getCursorType() {
569
594
  if (axCursorType && ![axCursorType isEqualToString:@"default"]) {
570
595
  finalType = axCursorType;
571
596
  } else if (systemCursorType && [systemCursorType length] > 0) {
572
- if ([systemCursorType isEqualToString:@"pointer"] &&
573
- (!axCursorType || [axCursorType isEqualToString:@"default"])) {
574
- finalType = @"default";
575
- } else {
576
- finalType = systemCursorType;
577
- }
597
+ // Prefer the system cursor when accessibility reports a generic value.
598
+ finalType = systemCursorType;
599
+ } else if (axCursorType && [axCursorType length] > 0) {
600
+ finalType = axCursorType;
578
601
  } else {
579
602
  finalType = @"default";
580
603
  }
@@ -629,17 +652,8 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
629
652
 
630
653
  CGPoint rawLocation = CGEventGetLocation(event);
631
654
 
632
- // Apply DPR scaling correction for Retina displays
633
- NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
655
+ // Coordinates are already in logical space; no additional scaling needed here.
634
656
  CGPoint location = rawLocation;
635
-
636
- if (scalingInfo) {
637
- CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
638
- NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
639
-
640
- // Keep logical coordinates - no scaling needed here
641
- location = rawLocation;
642
- }
643
657
  NSDate *currentDate = [NSDate date];
644
658
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
645
659
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
@@ -702,18 +716,9 @@ void cursorTimerCallback() {
702
716
  CFRelease(event);
703
717
  }
704
718
 
705
- // Apply DPR scaling correction for Retina displays
706
- NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
719
+ // Coordinates are already in logical space; no additional scaling needed here.
707
720
  CGPoint location = rawLocation;
708
721
 
709
- if (scalingInfo) {
710
- CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
711
- NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
712
-
713
- // Keep logical coordinates - no scaling needed here
714
- location = rawLocation;
715
- }
716
-
717
722
  NSDate *currentDate = [NSDate date];
718
723
  NSTimeInterval timestamp = [currentDate timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
719
724
  NSTimeInterval unixTimeMs = [currentDate timeIntervalSince1970] * 1000; // unix timestamp in milliseconds
@@ -1042,16 +1047,8 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
1042
1047
  // Get display scaling information
1043
1048
  NSDictionary *scalingInfo = getDisplayScalingInfo(rawLocation);
1044
1049
  CGPoint logicalLocation = rawLocation;
1045
-
1046
- if (scalingInfo) {
1047
- CGFloat scaleFactor = [[scalingInfo objectForKey:@"scaleFactor"] doubleValue];
1048
- NSRect displayBounds = [[scalingInfo objectForKey:@"displayBounds"] rectValue];
1049
-
1050
- // CGEventGetLocation returns LOGICAL coordinates (correct for JS layer)
1051
- // Keep logical coordinates - transformation happens in JS layer
1052
- logicalLocation = rawLocation;
1053
- }
1054
-
1050
+ // CGEventGetLocation already returns logical coordinates; additional scaling happens in JS layer.
1051
+
1055
1052
  NSString *cursorType = getCursorType();
1056
1053
 
1057
1054
  // Mouse button state'ini kontrol et