summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m')
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m3277
1 files changed, 3277 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m
new file mode 100644
index 0000000..52943e8
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m
@@ -0,0 +1,3277 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#ifdef SDL_VIDEO_DRIVER_COCOA
24
25#include <float.h> // For FLT_MAX
26
27#include "../../events/SDL_dropevents_c.h"
28#include "../../events/SDL_keyboard_c.h"
29#include "../../events/SDL_mouse_c.h"
30#include "../../events/SDL_touch_c.h"
31#include "../../events/SDL_windowevents_c.h"
32#include "../SDL_sysvideo.h"
33
34#include "SDL_cocoamouse.h"
35#include "SDL_cocoaopengl.h"
36#include "SDL_cocoaopengles.h"
37#include "SDL_cocoavideo.h"
38
39#if 0
40#define DEBUG_COCOAWINDOW
41#endif
42
43#ifdef DEBUG_COCOAWINDOW
44#define DLog(fmt, ...) printf("%s: " fmt "\n", __func__, ##__VA_ARGS__)
45#else
46#define DLog(...) \
47 do { \
48 } while (0)
49#endif
50
51#ifndef MAC_OS_X_VERSION_10_12
52#define NSEventModifierFlagCapsLock NSAlphaShiftKeyMask
53#endif
54#ifndef NSAppKitVersionNumber10_13_2
55#define NSAppKitVersionNumber10_13_2 1561.2
56#endif
57#ifndef NSAppKitVersionNumber10_14
58#define NSAppKitVersionNumber10_14 1671
59#endif
60
61@implementation SDL_CocoaWindowData
62
63@end
64
65@interface NSScreen (SDL)
66#if MAC_OS_X_VERSION_MAX_ALLOWED < 120000 // Added in the 12.0 SDK
67@property(readonly) NSEdgeInsets safeAreaInsets;
68#endif
69@end
70
71@interface NSWindow (SDL)
72// This is available as of 10.13.2, but isn't in public headers
73@property(nonatomic) NSRect mouseConfinementRect;
74@end
75
76@interface SDL3Window : NSWindow <NSDraggingDestination>
77// These are needed for borderless/fullscreen windows
78- (BOOL)canBecomeKeyWindow;
79- (BOOL)canBecomeMainWindow;
80- (void)sendEvent:(NSEvent *)event;
81- (void)doCommandBySelector:(SEL)aSelector;
82
83// Handle drag-and-drop of files onto the SDL window.
84- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender;
85- (void)draggingExited:(id<NSDraggingInfo>)sender;
86- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender;
87- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender;
88- (BOOL)wantsPeriodicDraggingUpdates;
89- (BOOL)validateMenuItem:(NSMenuItem *)menuItem;
90
91- (SDL_Window *)findSDLWindow;
92@end
93
94@implementation SDL3Window
95
96- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
97{
98 /* Only allow using the macOS native fullscreen toggle menubar item if the
99 * window is resizable and not in a SDL fullscreen mode.
100 */
101 if ([menuItem action] == @selector(toggleFullScreen:)) {
102 SDL_Window *window = [self findSDLWindow];
103 if (!window) {
104 return NO;
105 }
106
107 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
108 if ((window->flags & SDL_WINDOW_FULLSCREEN) && ![data.listener isInFullscreenSpace]) {
109 return NO;
110 } else if (!(window->flags & SDL_WINDOW_RESIZABLE)) {
111 return NO;
112 }
113 }
114 return [super validateMenuItem:menuItem];
115}
116
117- (BOOL)canBecomeKeyWindow
118{
119 SDL_Window *window = [self findSDLWindow];
120 if (window && !(window->flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_NOT_FOCUSABLE))) {
121 return YES;
122 } else {
123 return NO;
124 }
125}
126
127- (BOOL)canBecomeMainWindow
128{
129 SDL_Window *window = [self findSDLWindow];
130 if (window && !(window->flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_NOT_FOCUSABLE)) && !SDL_WINDOW_IS_POPUP(window)) {
131 return YES;
132 } else {
133 return NO;
134 }
135}
136
137- (void)sendEvent:(NSEvent *)event
138{
139 id delegate;
140 [super sendEvent:event];
141
142 if ([event type] != NSEventTypeLeftMouseUp) {
143 return;
144 }
145
146 delegate = [self delegate];
147 if (![delegate isKindOfClass:[SDL3Cocoa_WindowListener class]]) {
148 return;
149 }
150
151 if ([delegate isMoving]) {
152 [delegate windowDidFinishMoving];
153 }
154}
155
156/* We'll respond to selectors by doing nothing so we don't beep.
157 * The escape key gets converted to a "cancel" selector, etc.
158 */
159- (void)doCommandBySelector:(SEL)aSelector
160{
161 // NSLog(@"doCommandBySelector: %@\n", NSStringFromSelector(aSelector));
162}
163
164- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
165{
166 if (([sender draggingSourceOperationMask] & NSDragOperationGeneric) == NSDragOperationGeneric) {
167 return NSDragOperationGeneric;
168 } else if (([sender draggingSourceOperationMask] & NSDragOperationCopy) == NSDragOperationCopy) {
169 return NSDragOperationCopy;
170 }
171
172 return NSDragOperationNone; // no idea what to do with this, reject it.
173}
174
175- (void)draggingExited:(id<NSDraggingInfo>)sender
176{
177 SDL_Window *sdlwindow = [self findSDLWindow];
178 SDL_SendDropComplete(sdlwindow);
179}
180
181- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
182{
183 if (([sender draggingSourceOperationMask] & NSDragOperationGeneric) == NSDragOperationGeneric) {
184 SDL_Window *sdlwindow = [self findSDLWindow];
185 NSPoint point = [sender draggingLocation];
186 float x, y;
187 x = point.x;
188 y = (sdlwindow->h - point.y);
189 SDL_SendDropPosition(sdlwindow, x, y);
190 return NSDragOperationGeneric;
191 } else if (([sender draggingSourceOperationMask] & NSDragOperationCopy) == NSDragOperationCopy) {
192 SDL_Window *sdlwindow = [self findSDLWindow];
193 NSPoint point = [sender draggingLocation];
194 float x, y;
195 x = point.x;
196 y = (sdlwindow->h - point.y);
197 SDL_SendDropPosition(sdlwindow, x, y);
198 return NSDragOperationCopy;
199 }
200
201 return NSDragOperationNone; // no idea what to do with this, reject it.
202}
203
204- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
205{
206 SDL_LogTrace(SDL_LOG_CATEGORY_INPUT,
207 ". [SDL] In performDragOperation, draggingSourceOperationMask %lx, "
208 "expected Generic %lx, others Copy %lx, Link %lx, Private %lx, Move %lx, Delete %lx\n",
209 (unsigned long)[sender draggingSourceOperationMask],
210 (unsigned long)NSDragOperationGeneric,
211 (unsigned long)NSDragOperationCopy,
212 (unsigned long)NSDragOperationLink,
213 (unsigned long)NSDragOperationPrivate,
214 (unsigned long)NSDragOperationMove,
215 (unsigned long)NSDragOperationDelete);
216 if ([sender draggingPasteboard]) {
217 SDL_LogTrace(SDL_LOG_CATEGORY_INPUT,
218 ". [SDL] In performDragOperation, valid draggingPasteboard, "
219 "name [%s] '%s', changeCount %ld\n",
220 [[[[sender draggingPasteboard] name] className] UTF8String],
221 [[[[sender draggingPasteboard] name] description] UTF8String],
222 (long)[[sender draggingPasteboard] changeCount]);
223 }
224 @autoreleasepool {
225 NSPasteboard *pasteboard = [sender draggingPasteboard];
226 NSString *desiredType = [pasteboard availableTypeFromArray:@[ NSFilenamesPboardType, NSPasteboardTypeString ]];
227 SDL_Window *sdlwindow = [self findSDLWindow];
228 NSData *pboardData;
229 id pboardPlist;
230 NSString *pboardString;
231 NSPoint point;
232 float x, y;
233
234 for (NSString *supportedType in [pasteboard types]) {
235 NSString *typeString = [pasteboard stringForType:supportedType];
236 SDL_LogTrace(SDL_LOG_CATEGORY_INPUT,
237 ". [SDL] In performDragOperation, Pasteboard type '%s', stringForType (%lu) '%s'\n",
238 [[supportedType description] UTF8String],
239 (unsigned long)[[typeString description] length],
240 [[typeString description] UTF8String]);
241 }
242
243 if (desiredType == nil) {
244 return NO; // can't accept anything that's being dropped here.
245 }
246 pboardData = [pasteboard dataForType:desiredType];
247 if (pboardData == nil) {
248 return NO;
249 }
250 SDL_assert([desiredType isEqualToString:NSFilenamesPboardType] ||
251 [desiredType isEqualToString:NSPasteboardTypeString]);
252
253 pboardString = [pasteboard stringForType:desiredType];
254 pboardPlist = [pasteboard propertyListForType:desiredType];
255
256 // Use SendDropPosition to update the mouse location
257 point = [sender draggingLocation];
258 x = point.x;
259 y = (sdlwindow->h - point.y);
260 if (x >= 0.0f && x < (float)sdlwindow->w && y >= 0.0f && y < (float)sdlwindow->h) {
261 SDL_SendDropPosition(sdlwindow, x, y);
262 }
263 // Use SendDropPosition to update the mouse location
264
265 if ([desiredType isEqualToString:NSFilenamesPboardType]) {
266 for (NSString *path in (NSArray *)pboardPlist) {
267 NSURL *fileURL = [NSURL fileURLWithPath:path];
268 NSNumber *isAlias = nil;
269
270 [fileURL getResourceValue:&isAlias forKey:NSURLIsAliasFileKey error:nil];
271
272 // If the URL is an alias, resolve it.
273 if ([isAlias boolValue]) {
274 NSURLBookmarkResolutionOptions opts = NSURLBookmarkResolutionWithoutMounting |
275 NSURLBookmarkResolutionWithoutUI;
276 NSData *bookmark = [NSURL bookmarkDataWithContentsOfURL:fileURL error:nil];
277 if (bookmark != nil) {
278 NSURL *resolvedURL = [NSURL URLByResolvingBookmarkData:bookmark
279 options:opts
280 relativeToURL:nil
281 bookmarkDataIsStale:nil
282 error:nil];
283 if (resolvedURL != nil) {
284 fileURL = resolvedURL;
285 }
286 }
287 }
288 SDL_LogTrace(SDL_LOG_CATEGORY_INPUT,
289 ". [SDL] In performDragOperation, desiredType '%s', "
290 "Submitting DropFile as (%lu) '%s'\n",
291 [[desiredType description] UTF8String],
292 (unsigned long)[[[fileURL path] description] length],
293 [[[fileURL path] description] UTF8String]);
294 if (!SDL_SendDropFile(sdlwindow, NULL, [[[fileURL path] description] UTF8String])) {
295 return NO;
296 }
297 }
298 } else if ([desiredType isEqualToString:NSPasteboardTypeString]) {
299 char *buffer = SDL_strdup([[pboardString description] UTF8String]);
300 char *saveptr = NULL;
301 char *token = SDL_strtok_r(buffer, "\r\n", &saveptr);
302 while (token) {
303 SDL_LogTrace(SDL_LOG_CATEGORY_INPUT,
304 ". [SDL] In performDragOperation, desiredType '%s', "
305 "Submitting DropText as (%lu) '%s'\n",
306 [[desiredType description] UTF8String],
307 SDL_strlen(token), token);
308 if (!SDL_SendDropText(sdlwindow, token)) {
309 SDL_free(buffer);
310 return NO;
311 }
312 token = SDL_strtok_r(NULL, "\r\n", &saveptr);
313 }
314 SDL_free(buffer);
315 }
316
317 SDL_SendDropComplete(sdlwindow);
318 return YES;
319 }
320}
321
322- (BOOL)wantsPeriodicDraggingUpdates
323{
324 return NO;
325}
326
327- (SDL_Window *)findSDLWindow
328{
329 SDL_Window *sdlwindow = NULL;
330 SDL_VideoDevice *_this = SDL_GetVideoDevice();
331
332 // !!! FIXME: is there a better way to do this?
333 if (_this) {
334 for (sdlwindow = _this->windows; sdlwindow; sdlwindow = sdlwindow->next) {
335 NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)sdlwindow->internal).nswindow;
336 if (nswindow == self) {
337 break;
338 }
339 }
340 }
341
342 return sdlwindow;
343}
344
345@end
346
347bool b_inModeTransition;
348
349static CGFloat SqDistanceToRect(const NSPoint *point, const NSRect *rect)
350{
351 NSPoint edge = *point;
352 CGFloat left = NSMinX(*rect), right = NSMaxX(*rect);
353 CGFloat bottom = NSMinX(*rect), top = NSMaxY(*rect);
354 NSPoint delta;
355
356 if (point->x < left) {
357 edge.x = left;
358 } else if (point->x > right) {
359 edge.x = right;
360 }
361
362 if (point->y < bottom) {
363 edge.y = bottom;
364 } else if (point->y > top) {
365 edge.y = top;
366 }
367
368 delta = NSMakePoint(edge.x - point->x, edge.y - point->y);
369 return delta.x * delta.x + delta.y * delta.y;
370}
371
372static NSScreen *ScreenForPoint(const NSPoint *point)
373{
374 NSScreen *screen;
375
376 // Do a quick check first to see if the point lies on a specific screen
377 for (NSScreen *candidate in [NSScreen screens]) {
378 if (NSPointInRect(*point, [candidate frame])) {
379 screen = candidate;
380 break;
381 }
382 }
383
384 // Find the screen the point is closest to
385 if (!screen) {
386 CGFloat closest = MAXFLOAT;
387 for (NSScreen *candidate in [NSScreen screens]) {
388 NSRect screenRect = [candidate frame];
389
390 CGFloat sqdist = SqDistanceToRect(point, &screenRect);
391 if (sqdist < closest) {
392 screen = candidate;
393 closest = sqdist;
394 }
395 }
396 }
397
398 return screen;
399}
400
401bool Cocoa_IsWindowInFullscreenSpace(SDL_Window *window)
402{
403 @autoreleasepool {
404 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
405
406 if ([data.listener isInFullscreenSpace]) {
407 return true;
408 } else {
409 return false;
410 }
411 }
412}
413
414bool Cocoa_IsWindowZoomed(SDL_Window *window)
415{
416 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
417 NSWindow *nswindow = data.nswindow;
418 bool zoomed = false;
419
420 // isZoomed always returns true if the window is not resizable or the window is fullscreen
421 if ((window->flags & SDL_WINDOW_RESIZABLE) && [nswindow isZoomed] &&
422 !(window->flags & SDL_WINDOW_FULLSCREEN) && !Cocoa_IsWindowInFullscreenSpace(window)) {
423 // If we are at our desired floating area, then we're not zoomed
424 bool floating = (window->x == window->floating.x &&
425 window->y == window->floating.y &&
426 window->w == window->floating.w &&
427 window->h == window->floating.h);
428 if (!floating) {
429 zoomed = true;
430 }
431 }
432 return zoomed;
433}
434
435typedef enum CocoaMenuVisibility
436{
437 COCOA_MENU_VISIBILITY_AUTO = 0,
438 COCOA_MENU_VISIBILITY_NEVER,
439 COCOA_MENU_VISIBILITY_ALWAYS
440} CocoaMenuVisibility;
441
442static CocoaMenuVisibility menu_visibility_hint = COCOA_MENU_VISIBILITY_AUTO;
443
444static void Cocoa_ToggleFullscreenSpaceMenuVisibility(SDL_Window *window)
445{
446 if (window && Cocoa_IsWindowInFullscreenSpace(window)) {
447 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
448
449 // 'Auto' sets the menu to visible if fullscreen wasn't explicitly entered via SDL_SetWindowFullscreen().
450 if ((menu_visibility_hint == COCOA_MENU_VISIBILITY_AUTO && !data.fullscreen_space_requested) ||
451 menu_visibility_hint == COCOA_MENU_VISIBILITY_ALWAYS) {
452 [NSMenu setMenuBarVisible:YES];
453 } else {
454 [NSMenu setMenuBarVisible:NO];
455 }
456 }
457}
458
459void Cocoa_MenuVisibilityCallback(void *userdata, const char *name, const char *oldValue, const char *newValue)
460{
461 if (newValue) {
462 if (*newValue == '0' || SDL_strcasecmp(newValue, "false") == 0) {
463 menu_visibility_hint = COCOA_MENU_VISIBILITY_NEVER;
464 } else if (*newValue == '1' || SDL_strcasecmp(newValue, "true") == 0) {
465 menu_visibility_hint = COCOA_MENU_VISIBILITY_ALWAYS;
466 } else {
467 menu_visibility_hint = COCOA_MENU_VISIBILITY_AUTO;
468 }
469 } else {
470 menu_visibility_hint = COCOA_MENU_VISIBILITY_AUTO;
471 }
472
473 // Update the current menu visibility.
474 Cocoa_ToggleFullscreenSpaceMenuVisibility(SDL_GetKeyboardFocus());
475}
476
477static NSScreen *ScreenForRect(const NSRect *rect)
478{
479 NSPoint center = NSMakePoint(NSMidX(*rect), NSMidY(*rect));
480 return ScreenForPoint(&center);
481}
482
483static void ConvertNSRect(NSRect *r)
484{
485 r->origin.y = CGDisplayPixelsHigh(kCGDirectMainDisplay) - r->origin.y - r->size.height;
486}
487
488static void ScheduleContextUpdates(SDL_CocoaWindowData *data)
489{
490// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it.
491#ifdef SDL_VIDEO_OPENGL
492
493#ifdef __clang__
494#pragma clang diagnostic push
495#pragma clang diagnostic ignored "-Wdeprecated-declarations"
496#endif
497
498 NSOpenGLContext *currentContext;
499 NSMutableArray *contexts;
500 if (!data || !data.nscontexts) {
501 return;
502 }
503
504 currentContext = [NSOpenGLContext currentContext];
505 contexts = data.nscontexts;
506 @synchronized(contexts) {
507 for (SDL3OpenGLContext *context in contexts) {
508 if (context == currentContext) {
509 [context update];
510 } else {
511 [context scheduleUpdate];
512 }
513 }
514 }
515
516#ifdef __clang__
517#pragma clang diagnostic pop
518#endif
519
520#endif // SDL_VIDEO_OPENGL
521}
522
523// !!! FIXME: this should use a hint callback.
524static bool GetHintCtrlClickEmulateRightClick(void)
525{
526 return SDL_GetHintBoolean(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, false);
527}
528
529static NSUInteger GetWindowWindowedStyle(SDL_Window *window)
530{
531 /* IF YOU CHANGE ANY FLAGS IN HERE, PLEASE READ
532 the NSWindowStyleMaskBorderless comments in SetupWindowData()! */
533
534 /* always allow miniaturization, otherwise you can't programmatically
535 minimize the window, whether there's a title bar or not */
536 NSUInteger style = NSWindowStyleMaskMiniaturizable;
537
538 if (!SDL_WINDOW_IS_POPUP(window)) {
539 if (window->flags & SDL_WINDOW_BORDERLESS) {
540 style |= NSWindowStyleMaskBorderless;
541 } else {
542 style |= (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable);
543 }
544 if (window->flags & SDL_WINDOW_RESIZABLE) {
545 style |= NSWindowStyleMaskResizable;
546 }
547 } else {
548 style |= NSWindowStyleMaskBorderless;
549 }
550 return style;
551}
552
553static NSUInteger GetWindowStyle(SDL_Window *window)
554{
555 NSUInteger style = 0;
556
557 if (window->flags & SDL_WINDOW_FULLSCREEN) {
558 style = NSWindowStyleMaskBorderless;
559 } else {
560 style = GetWindowWindowedStyle(window);
561 }
562 return style;
563}
564
565static bool SetWindowStyle(SDL_Window *window, NSUInteger style)
566{
567 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
568 NSWindow *nswindow = data.nswindow;
569
570 // The view responder chain gets messed with during setStyleMask
571 if ([data.sdlContentView nextResponder] == data.listener) {
572 [data.sdlContentView setNextResponder:nil];
573 }
574
575 [nswindow setStyleMask:style];
576
577 // The view responder chain gets messed with during setStyleMask
578 if ([data.sdlContentView nextResponder] != data.listener) {
579 [data.sdlContentView setNextResponder:data.listener];
580 }
581
582 return true;
583}
584
585static bool ShouldAdjustCoordinatesForGrab(SDL_Window *window)
586{
587 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
588
589 if (!data || [data.listener isMovingOrFocusClickPending]) {
590 return false;
591 }
592
593 if (!(window->flags & SDL_WINDOW_INPUT_FOCUS)) {
594 return false;
595 }
596
597 if ((window->flags & SDL_WINDOW_MOUSE_GRABBED) || (window->mouse_rect.w > 0 && window->mouse_rect.h > 0)) {
598 return true;
599 }
600 return false;
601}
602
603static bool AdjustCoordinatesForGrab(SDL_Window *window, float x, float y, CGPoint *adjusted)
604{
605 if (window->mouse_rect.w > 0 && window->mouse_rect.h > 0) {
606 SDL_Rect window_rect;
607 SDL_Rect mouse_rect;
608
609 window_rect.x = 0;
610 window_rect.y = 0;
611 window_rect.w = window->w;
612 window_rect.h = window->h;
613
614 if (SDL_GetRectIntersection(&window->mouse_rect, &window_rect, &mouse_rect)) {
615 float left = (float)window->x + mouse_rect.x;
616 float right = left + mouse_rect.w - 1;
617 float top = (float)window->y + mouse_rect.y;
618 float bottom = top + mouse_rect.h - 1;
619 if (x < left || x > right || y < top || y > bottom) {
620 adjusted->x = SDL_clamp(x, left, right);
621 adjusted->y = SDL_clamp(y, top, bottom);
622 return true;
623 }
624 return false;
625 }
626 }
627
628 if (window->flags & SDL_WINDOW_MOUSE_GRABBED) {
629 float left = (float)window->x;
630 float right = left + window->w - 1;
631 float top = (float)window->y;
632 float bottom = top + window->h - 1;
633 if (x < left || x > right || y < top || y > bottom) {
634 adjusted->x = SDL_clamp(x, left, right);
635 adjusted->y = SDL_clamp(y, top, bottom);
636 return true;
637 }
638 }
639 return false;
640}
641
642static void Cocoa_UpdateClipCursor(SDL_Window *window)
643{
644 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
645
646 if (NSAppKitVersionNumber >= NSAppKitVersionNumber10_13_2) {
647 NSWindow *nswindow = data.nswindow;
648 SDL_Rect mouse_rect;
649
650 SDL_zero(mouse_rect);
651
652 if (ShouldAdjustCoordinatesForGrab(window)) {
653 SDL_Rect window_rect;
654
655 window_rect.x = 0;
656 window_rect.y = 0;
657 window_rect.w = window->w;
658 window_rect.h = window->h;
659
660 if (window->mouse_rect.w > 0 && window->mouse_rect.h > 0) {
661 SDL_GetRectIntersection(&window->mouse_rect, &window_rect, &mouse_rect);
662 }
663
664 if ((window->flags & SDL_WINDOW_MOUSE_GRABBED) != 0 &&
665 SDL_RectEmpty(&mouse_rect)) {
666 SDL_memcpy(&mouse_rect, &window_rect, sizeof(mouse_rect));
667 }
668 }
669
670 if (SDL_RectEmpty(&mouse_rect)) {
671 nswindow.mouseConfinementRect = NSZeroRect;
672 } else {
673 NSRect rect;
674 rect.origin.x = mouse_rect.x;
675 rect.origin.y = [nswindow contentLayoutRect].size.height - mouse_rect.y - mouse_rect.h;
676 rect.size.width = mouse_rect.w;
677 rect.size.height = mouse_rect.h;
678 nswindow.mouseConfinementRect = rect;
679 }
680 } else {
681 // Move the cursor to the nearest point in the window
682 if (ShouldAdjustCoordinatesForGrab(window)) {
683 float x, y;
684 CGPoint cgpoint;
685
686 SDL_GetGlobalMouseState(&x, &y);
687 if (AdjustCoordinatesForGrab(window, x, y, &cgpoint)) {
688 Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
689 CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
690 }
691 }
692 }
693}
694
695static SDL_Window *GetParentToplevelWindow(SDL_Window *window)
696{
697 SDL_Window *toplevel = window;
698
699 // Find the topmost parent
700 while (SDL_WINDOW_IS_POPUP(toplevel)) {
701 toplevel = toplevel->parent;
702 }
703
704 return toplevel;
705}
706
707static void Cocoa_SetKeyboardFocus(SDL_Window *window, bool set_active_focus)
708{
709 SDL_Window *toplevel = GetParentToplevelWindow(window);
710 SDL_CocoaWindowData *toplevel_data;
711
712 toplevel_data = (__bridge SDL_CocoaWindowData *)toplevel->internal;
713 toplevel_data.keyboard_focus = window;
714
715 if (set_active_focus && !window->is_hiding && !window->is_destroying) {
716 SDL_SetKeyboardFocus(window);
717 }
718}
719
720static void Cocoa_SendExposedEventIfVisible(SDL_Window *window)
721{
722 NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow;
723 if ([nswindow occlusionState] & NSWindowOcclusionStateVisible) {
724 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
725 }
726}
727
728static void Cocoa_WaitForMiniaturizable(SDL_Window *window)
729{
730 NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow;
731 NSButton *button = [nswindow standardWindowButton:NSWindowMiniaturizeButton];
732 if (button) {
733 int iterations = 0;
734 while (![button isEnabled] && (iterations < 100)) {
735 SDL_Delay(10);
736 SDL_PumpEvents();
737 iterations++;
738 }
739 }
740}
741
742static NSCursor *Cocoa_GetDesiredCursor(void)
743{
744 SDL_Mouse *mouse = SDL_GetMouse();
745
746 if (mouse->cursor_shown && mouse->cur_cursor && !mouse->relative_mode) {
747 return (__bridge NSCursor *)mouse->cur_cursor->internal;
748 }
749
750 return [NSCursor invisibleCursor];
751}
752
753@implementation SDL3Cocoa_WindowListener
754
755- (void)listen:(SDL_CocoaWindowData *)data
756{
757 NSNotificationCenter *center;
758 NSWindow *window = data.nswindow;
759 NSView *view = data.sdlContentView;
760
761 _data = data;
762 observingVisible = YES;
763 wasCtrlLeft = NO;
764 wasVisible = [window isVisible];
765 isFullscreenSpace = NO;
766 inFullscreenTransition = NO;
767 pendingWindowOperation = PENDING_OPERATION_NONE;
768 isMoving = NO;
769 isMiniaturizing = NO;
770 isDragAreaRunning = NO;
771 pendingWindowWarpX = pendingWindowWarpY = FLT_MAX;
772 liveResizeTimer = nil;
773
774 center = [NSNotificationCenter defaultCenter];
775
776 if ([window delegate] != nil) {
777 [center addObserver:self selector:@selector(windowDidExpose:) name:NSWindowDidExposeNotification object:window];
778 [center addObserver:self selector:@selector(windowDidChangeOcclusionState:) name:NSWindowDidChangeOcclusionStateNotification object:window];
779 [center addObserver:self selector:@selector(windowWillStartLiveResize:) name:NSWindowWillStartLiveResizeNotification object:window];
780 [center addObserver:self selector:@selector(windowDidEndLiveResize:) name:NSWindowDidEndLiveResizeNotification object:window];
781 [center addObserver:self selector:@selector(windowWillMove:) name:NSWindowWillMoveNotification object:window];
782 [center addObserver:self selector:@selector(windowDidMove:) name:NSWindowDidMoveNotification object:window];
783 [center addObserver:self selector:@selector(windowDidResize:) name:NSWindowDidResizeNotification object:window];
784 [center addObserver:self selector:@selector(windowWillMiniaturize:) name:NSWindowWillMiniaturizeNotification object:window];
785 [center addObserver:self selector:@selector(windowDidMiniaturize:) name:NSWindowDidMiniaturizeNotification object:window];
786 [center addObserver:self selector:@selector(windowDidDeminiaturize:) name:NSWindowDidDeminiaturizeNotification object:window];
787 [center addObserver:self selector:@selector(windowDidBecomeKey:) name:NSWindowDidBecomeKeyNotification object:window];
788 [center addObserver:self selector:@selector(windowDidResignKey:) name:NSWindowDidResignKeyNotification object:window];
789 [center addObserver:self selector:@selector(windowDidChangeBackingProperties:) name:NSWindowDidChangeBackingPropertiesNotification object:window];
790 [center addObserver:self selector:@selector(windowDidChangeScreenProfile:) name:NSWindowDidChangeScreenProfileNotification object:window];
791 [center addObserver:self selector:@selector(windowDidChangeScreen:) name:NSWindowDidChangeScreenNotification object:window];
792 [center addObserver:self selector:@selector(windowWillEnterFullScreen:) name:NSWindowWillEnterFullScreenNotification object:window];
793 [center addObserver:self selector:@selector(windowDidEnterFullScreen:) name:NSWindowDidEnterFullScreenNotification object:window];
794 [center addObserver:self selector:@selector(windowWillExitFullScreen:) name:NSWindowWillExitFullScreenNotification object:window];
795 [center addObserver:self selector:@selector(windowDidExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:window];
796 [center addObserver:self selector:@selector(windowDidFailToEnterFullScreen:) name:@"NSWindowDidFailToEnterFullScreenNotification" object:window];
797 [center addObserver:self selector:@selector(windowDidFailToExitFullScreen:) name:@"NSWindowDidFailToExitFullScreenNotification" object:window];
798 } else {
799 [window setDelegate:self];
800 }
801
802 /* Haven't found a delegate / notification that triggers when the window is
803 * ordered out (is not visible any more). You can be ordered out without
804 * minimizing, so DidMiniaturize doesn't work. (e.g. -[NSWindow orderOut:])
805 */
806 [window addObserver:self
807 forKeyPath:@"visible"
808 options:NSKeyValueObservingOptionNew
809 context:NULL];
810
811 [window setNextResponder:self];
812 [window setAcceptsMouseMovedEvents:YES];
813
814 [view setNextResponder:self];
815
816 [view setAcceptsTouchEvents:YES];
817}
818
819- (void)observeValueForKeyPath:(NSString *)keyPath
820 ofObject:(id)object
821 change:(NSDictionary *)change
822 context:(void *)context
823{
824 if (!observingVisible) {
825 return;
826 }
827
828 if (object == _data.nswindow && [keyPath isEqualToString:@"visible"]) {
829 int newVisibility = [[change objectForKey:@"new"] intValue];
830 if (newVisibility) {
831 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_SHOWN, 0, 0);
832 } else if (![_data.nswindow isMiniaturized]) {
833 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIDDEN, 0, 0);
834 }
835 }
836}
837
838- (void)pauseVisibleObservation
839{
840 observingVisible = NO;
841 wasVisible = [_data.nswindow isVisible];
842}
843
844- (void)resumeVisibleObservation
845{
846 BOOL isVisible = [_data.nswindow isVisible];
847 observingVisible = YES;
848 if (wasVisible != isVisible) {
849 if (isVisible) {
850 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_SHOWN, 0, 0);
851 } else if (![_data.nswindow isMiniaturized]) {
852 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIDDEN, 0, 0);
853 }
854
855 wasVisible = isVisible;
856 }
857}
858
859- (BOOL)setFullscreenSpace:(BOOL)state
860{
861 SDL_Window *window = _data.window;
862 NSWindow *nswindow = _data.nswindow;
863 SDL_CocoaVideoData *videodata = ((__bridge SDL_CocoaWindowData *)window->internal).videodata;
864
865 if (!videodata.allow_spaces) {
866 return NO; // Spaces are forcibly disabled.
867 } else if (state && window->fullscreen_exclusive) {
868 return NO; // we only allow you to make a Space on fullscreen desktop windows.
869 } else if (!state && window->last_fullscreen_exclusive_display) {
870 return NO; // we only handle leaving the Space on windows that were previously fullscreen desktop.
871 } else if (state == isFullscreenSpace) {
872 return YES; // already there.
873 }
874
875 if (inFullscreenTransition) {
876 if (state) {
877 [self addPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN];
878 } else {
879 [self addPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN];
880 }
881 return YES;
882 }
883 inFullscreenTransition = YES;
884
885 // you need to be FullScreenPrimary, or toggleFullScreen doesn't work. Unset it again in windowDidExitFullScreen.
886 [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
887 [nswindow performSelectorOnMainThread:@selector(toggleFullScreen:) withObject:nswindow waitUntilDone:NO];
888 return YES;
889}
890
891- (BOOL)isInFullscreenSpace
892{
893 return isFullscreenSpace;
894}
895
896- (BOOL)isInFullscreenSpaceTransition
897{
898 return inFullscreenTransition;
899}
900
901- (void)clearPendingWindowOperation:(PendingWindowOperation)operation
902{
903 pendingWindowOperation &= ~operation;
904}
905
906- (void)addPendingWindowOperation:(PendingWindowOperation)operation
907{
908 pendingWindowOperation |= operation;
909}
910
911- (BOOL)windowOperationIsPending:(PendingWindowOperation)operation
912{
913 return !!(pendingWindowOperation & operation);
914}
915
916- (BOOL)hasPendingWindowOperation
917{
918 // A pending zoom may be deferred until leaving fullscreen, so don't block on it.
919 return (pendingWindowOperation & ~PENDING_OPERATION_ZOOM) != PENDING_OPERATION_NONE ||
920 isMiniaturizing || inFullscreenTransition;
921}
922
923- (void)close
924{
925 NSNotificationCenter *center;
926 NSWindow *window = _data.nswindow;
927 NSView *view = [window contentView];
928
929 center = [NSNotificationCenter defaultCenter];
930
931 if ([window delegate] != self) {
932 [center removeObserver:self name:NSWindowDidExposeNotification object:window];
933 [center removeObserver:self name:NSWindowDidChangeOcclusionStateNotification object:window];
934 [center removeObserver:self name:NSWindowWillStartLiveResizeNotification object:window];
935 [center removeObserver:self name:NSWindowDidEndLiveResizeNotification object:window];
936 [center removeObserver:self name:NSWindowWillMoveNotification object:window];
937 [center removeObserver:self name:NSWindowDidMoveNotification object:window];
938 [center removeObserver:self name:NSWindowDidResizeNotification object:window];
939 [center removeObserver:self name:NSWindowWillMiniaturizeNotification object:window];
940 [center removeObserver:self name:NSWindowDidMiniaturizeNotification object:window];
941 [center removeObserver:self name:NSWindowDidDeminiaturizeNotification object:window];
942 [center removeObserver:self name:NSWindowDidBecomeKeyNotification object:window];
943 [center removeObserver:self name:NSWindowDidResignKeyNotification object:window];
944 [center removeObserver:self name:NSWindowDidChangeBackingPropertiesNotification object:window];
945 [center removeObserver:self name:NSWindowDidChangeScreenProfileNotification object:window];
946 [center removeObserver:self name:NSWindowDidChangeScreenNotification object:window];
947 [center removeObserver:self name:NSWindowWillEnterFullScreenNotification object:window];
948 [center removeObserver:self name:NSWindowDidEnterFullScreenNotification object:window];
949 [center removeObserver:self name:NSWindowWillExitFullScreenNotification object:window];
950 [center removeObserver:self name:NSWindowDidExitFullScreenNotification object:window];
951 [center removeObserver:self name:@"NSWindowDidFailToEnterFullScreenNotification" object:window];
952 [center removeObserver:self name:@"NSWindowDidFailToExitFullScreenNotification" object:window];
953 } else {
954 [window setDelegate:nil];
955 }
956
957 [window removeObserver:self forKeyPath:@"visible"];
958
959 if ([window nextResponder] == self) {
960 [window setNextResponder:nil];
961 }
962 if ([view nextResponder] == self) {
963 [view setNextResponder:nil];
964 }
965}
966
967- (BOOL)isMoving
968{
969 return isMoving;
970}
971
972- (BOOL)isMovingOrFocusClickPending
973{
974 return isMoving || (focusClickPending != 0);
975}
976
977- (void)setFocusClickPending:(NSInteger)button
978{
979 focusClickPending |= (1 << button);
980}
981
982- (void)clearFocusClickPending:(NSInteger)button
983{
984 if (focusClickPending & (1 << button)) {
985 focusClickPending &= ~(1 << button);
986 if (focusClickPending == 0) {
987 [self onMovingOrFocusClickPendingStateCleared];
988 }
989 }
990}
991
992- (void)updateIgnoreMouseState:(NSEvent *)theEvent
993{
994 SDL_Window *window = _data.window;
995 SDL_Surface *shape = (SDL_Surface *)SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_SHAPE_POINTER, NULL);
996 BOOL ignoresMouseEvents = NO;
997
998 if (shape) {
999 NSPoint point = [theEvent locationInWindow];
1000 NSRect windowRect = [[_data.nswindow contentView] frame];
1001 if (NSMouseInRect(point, windowRect, NO)) {
1002 int x = (int)SDL_roundf((point.x / (window->w - 1)) * (shape->w - 1));
1003 int y = (int)SDL_roundf(((window->h - point.y) / (window->h - 1)) * (shape->h - 1));
1004 Uint8 a;
1005
1006 if (!SDL_ReadSurfacePixel(shape, x, y, NULL, NULL, NULL, &a) || a == SDL_ALPHA_TRANSPARENT) {
1007 ignoresMouseEvents = YES;
1008 }
1009 }
1010 }
1011 _data.nswindow.ignoresMouseEvents = ignoresMouseEvents;
1012}
1013
1014- (void)setPendingMoveX:(float)x Y:(float)y
1015{
1016 pendingWindowWarpX = x;
1017 pendingWindowWarpY = y;
1018}
1019
1020- (void)windowDidFinishMoving
1021{
1022 if (isMoving) {
1023 isMoving = NO;
1024 [self onMovingOrFocusClickPendingStateCleared];
1025 }
1026}
1027
1028- (void)onMovingOrFocusClickPendingStateCleared
1029{
1030 if (![self isMovingOrFocusClickPending]) {
1031 SDL_Mouse *mouse = SDL_GetMouse();
1032 if (pendingWindowWarpX != FLT_MAX && pendingWindowWarpY != FLT_MAX) {
1033 mouse->WarpMouseGlobal(pendingWindowWarpX, pendingWindowWarpY);
1034 pendingWindowWarpX = pendingWindowWarpY = FLT_MAX;
1035 }
1036 if (mouse->relative_mode && mouse->focus == _data.window) {
1037 // Move the cursor to the nearest point in the window
1038 {
1039 float x, y;
1040 CGPoint cgpoint;
1041
1042 SDL_GetMouseState(&x, &y);
1043 cgpoint.x = _data.window->x + x;
1044 cgpoint.y = _data.window->y + y;
1045
1046 Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
1047
1048 DLog("Returning cursor to (%g, %g)", cgpoint.x, cgpoint.y);
1049 CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
1050 }
1051
1052 mouse->SetRelativeMouseMode(true);
1053 } else {
1054 Cocoa_UpdateClipCursor(_data.window);
1055 }
1056 }
1057}
1058
1059- (BOOL)windowShouldClose:(id)sender
1060{
1061 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_CLOSE_REQUESTED, 0, 0);
1062 return NO;
1063}
1064
1065- (void)windowDidExpose:(NSNotification *)aNotification
1066{
1067 Cocoa_SendExposedEventIfVisible(_data.window);
1068}
1069
1070- (void)windowDidChangeOcclusionState:(NSNotification *)aNotification
1071{
1072 if ([_data.nswindow occlusionState] & NSWindowOcclusionStateVisible) {
1073 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
1074 } else {
1075 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_OCCLUDED, 0, 0);
1076 }
1077}
1078
1079- (void)windowWillStartLiveResize:(NSNotification *)aNotification
1080{
1081 // We'll try to maintain 60 FPS during live resizing
1082 const NSTimeInterval interval = 1.0 / 60.0;
1083 liveResizeTimer = [NSTimer scheduledTimerWithTimeInterval:interval
1084 repeats:TRUE
1085 block:^(NSTimer *unusedTimer)
1086 {
1087 SDL_OnWindowLiveResizeUpdate(_data.window);
1088 }];
1089
1090 [[NSRunLoop currentRunLoop] addTimer:liveResizeTimer forMode:NSRunLoopCommonModes];
1091}
1092
1093- (void)windowDidEndLiveResize:(NSNotification *)aNotification
1094{
1095 [liveResizeTimer invalidate];
1096 liveResizeTimer = nil;
1097}
1098
1099- (void)windowWillMove:(NSNotification *)aNotification
1100{
1101 if ([_data.nswindow isKindOfClass:[SDL3Window class]]) {
1102 pendingWindowWarpX = pendingWindowWarpY = FLT_MAX;
1103 isMoving = YES;
1104 }
1105}
1106
1107- (void)windowDidMove:(NSNotification *)aNotification
1108{
1109 int x, y;
1110 SDL_Window *window = _data.window;
1111 NSWindow *nswindow = _data.nswindow;
1112 NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]];
1113 ConvertNSRect(&rect);
1114
1115 if (inFullscreenTransition || b_inModeTransition) {
1116 // We'll take care of this at the end of the transition
1117 return;
1118 }
1119
1120 x = (int)rect.origin.x;
1121 y = (int)rect.origin.y;
1122
1123 ScheduleContextUpdates(_data);
1124
1125 // Get the parent-relative coordinates for child windows.
1126 SDL_GlobalToRelativeForWindow(window, x, y, &x, &y);
1127 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MOVED, x, y);
1128}
1129
1130- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize
1131{
1132 SDL_Window *window = _data.window;
1133
1134 if (window->min_aspect != window->max_aspect) {
1135 NSWindow *nswindow = _data.nswindow;
1136 NSRect newContentRect = [nswindow contentRectForFrameRect:NSMakeRect(0, 0, frameSize.width, frameSize.height)];
1137 NSSize newSize = newContentRect.size;
1138 CGFloat minAspectRatio = window->min_aspect;
1139 CGFloat maxAspectRatio = window->max_aspect;
1140 CGFloat aspectRatio;
1141
1142 if (newSize.height > 0) {
1143 aspectRatio = newSize.width / newSize.height;
1144
1145 if (maxAspectRatio > 0.0f && aspectRatio > maxAspectRatio) {
1146 newSize.width = SDL_roundf(newSize.height * maxAspectRatio);
1147 } else if (minAspectRatio > 0.0f && aspectRatio < minAspectRatio) {
1148 newSize.height = SDL_roundf(newSize.width / minAspectRatio);
1149 }
1150
1151 NSRect newFrameRect = [nswindow frameRectForContentRect:NSMakeRect(0, 0, newSize.width, newSize.height)];
1152 frameSize = newFrameRect.size;
1153 }
1154 }
1155 return frameSize;
1156}
1157
1158- (void)windowDidResize:(NSNotification *)aNotification
1159{
1160 SDL_Window *window;
1161 NSWindow *nswindow;
1162 NSRect rect;
1163 int x, y, w, h;
1164 BOOL zoomed;
1165
1166 if (inFullscreenTransition || b_inModeTransition) {
1167 // We'll take care of this at the end of the transition
1168 return;
1169 }
1170
1171 if (focusClickPending) {
1172 focusClickPending = 0;
1173 [self onMovingOrFocusClickPendingStateCleared];
1174 }
1175 window = _data.window;
1176 nswindow = _data.nswindow;
1177 rect = [nswindow contentRectForFrameRect:[nswindow frame]];
1178 ConvertNSRect(&rect);
1179 x = (int)rect.origin.x;
1180 y = (int)rect.origin.y;
1181 w = (int)rect.size.width;
1182 h = (int)rect.size.height;
1183
1184 ScheduleContextUpdates(_data);
1185
1186 /* isZoomed always returns true if the window is not resizable
1187 * and fullscreen windows are considered zoomed.
1188 */
1189 if ((window->flags & SDL_WINDOW_RESIZABLE) && [nswindow isZoomed] &&
1190 !(window->flags & SDL_WINDOW_FULLSCREEN) && ![self isInFullscreenSpace]) {
1191 zoomed = YES;
1192 } else {
1193 zoomed = NO;
1194 }
1195 if (!zoomed) {
1196 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESTORED, 0, 0);
1197 } else {
1198 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MAXIMIZED, 0, 0);
1199 if ([self windowOperationIsPending:PENDING_OPERATION_MINIMIZE]) {
1200 [nswindow miniaturize:nil];
1201 }
1202 }
1203
1204 /* The window can move during a resize event, such as when maximizing
1205 or resizing from a corner */
1206 SDL_GlobalToRelativeForWindow(window, x, y, &x, &y);
1207 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MOVED, x, y);
1208 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, w, h);
1209}
1210
1211- (void)windowWillMiniaturize:(NSNotification *)aNotification
1212{
1213 isMiniaturizing = YES;
1214 Cocoa_WaitForMiniaturizable(_data.window);
1215}
1216
1217- (void)windowDidMiniaturize:(NSNotification *)aNotification
1218{
1219 if (focusClickPending) {
1220 focusClickPending = 0;
1221 [self onMovingOrFocusClickPendingStateCleared];
1222 }
1223 isMiniaturizing = NO;
1224 [self clearPendingWindowOperation:PENDING_OPERATION_MINIMIZE];
1225 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_MINIMIZED, 0, 0);
1226}
1227
1228- (void)windowDidDeminiaturize:(NSNotification *)aNotification
1229{
1230 // Always send restored before maximized.
1231 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_RESTORED, 0, 0);
1232
1233 if (Cocoa_IsWindowZoomed(_data.window)) {
1234 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_MAXIMIZED, 0, 0);
1235 }
1236
1237 if ([self windowOperationIsPending:PENDING_OPERATION_ENTER_FULLSCREEN]) {
1238 SDL_UpdateFullscreenMode(_data.window, true, true);
1239 }
1240}
1241
1242- (void)windowDidBecomeKey:(NSNotification *)aNotification
1243{
1244 SDL_Window *window = _data.window;
1245
1246 // We're going to get keyboard events, since we're key.
1247 // This needs to be done before restoring the relative mouse mode.
1248 Cocoa_SetKeyboardFocus(_data.keyboard_focus ? _data.keyboard_focus : window, true);
1249
1250 // If we just gained focus we need the updated mouse position
1251 if (!(window->flags & SDL_WINDOW_MOUSE_RELATIVE_MODE)) {
1252 NSPoint point;
1253 float x, y;
1254
1255 point = [_data.nswindow mouseLocationOutsideOfEventStream];
1256 x = point.x;
1257 y = (window->h - point.y);
1258
1259 if (x >= 0.0f && x < (float)window->w && y >= 0.0f && y < (float)window->h) {
1260 SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, false, x, y);
1261 }
1262 }
1263
1264 // Check to see if someone updated the clipboard
1265 Cocoa_CheckClipboardUpdate(_data.videodata);
1266
1267 if (isFullscreenSpace && !window->fullscreen_exclusive) {
1268 Cocoa_ToggleFullscreenSpaceMenuVisibility(window);
1269 }
1270 {
1271 const unsigned int newflags = [NSEvent modifierFlags] & NSEventModifierFlagCapsLock;
1272 _data.videodata.modifierFlags = (_data.videodata.modifierFlags & ~NSEventModifierFlagCapsLock) | newflags;
1273 SDL_ToggleModState(SDL_KMOD_CAPS, newflags ? true : false);
1274 }
1275
1276 /* Restore fullscreen mode unless the window is deminiaturizing.
1277 * If it is, fullscreen will be restored when deminiaturization is complete.
1278 */
1279 if (!(window->flags & SDL_WINDOW_MINIMIZED) &&
1280 [self windowOperationIsPending:PENDING_OPERATION_ENTER_FULLSCREEN]) {
1281 SDL_UpdateFullscreenMode(window, true, true);
1282 }
1283}
1284
1285- (void)windowDidResignKey:(NSNotification *)aNotification
1286{
1287 // Some other window will get mouse events, since we're not key.
1288 if (SDL_GetMouseFocus() == _data.window) {
1289 SDL_SetMouseFocus(NULL);
1290 }
1291
1292 // Some other window will get keyboard events, since we're not key.
1293 if (SDL_GetKeyboardFocus() == _data.window) {
1294 SDL_SetKeyboardFocus(NULL);
1295 }
1296
1297 if (isFullscreenSpace) {
1298 [NSMenu setMenuBarVisible:YES];
1299 }
1300}
1301
1302- (void)windowDidChangeBackingProperties:(NSNotification *)aNotification
1303{
1304 NSNumber *oldscale = [[aNotification userInfo] objectForKey:NSBackingPropertyOldScaleFactorKey];
1305
1306 if (inFullscreenTransition) {
1307 return;
1308 }
1309
1310 if ([oldscale doubleValue] != [_data.nswindow backingScaleFactor]) {
1311 // Send a resize event when the backing scale factor changes.
1312 [self windowDidResize:aNotification];
1313 }
1314}
1315
1316- (void)windowDidChangeScreenProfile:(NSNotification *)aNotification
1317{
1318 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_ICCPROF_CHANGED, 0, 0);
1319}
1320
1321- (void)windowDidChangeScreen:(NSNotification *)aNotification
1322{
1323 // printf("WINDOWDIDCHANGESCREEN\n");
1324
1325#ifdef SDL_VIDEO_OPENGL
1326
1327 if (_data && _data.nscontexts) {
1328 for (SDL3OpenGLContext *context in _data.nscontexts) {
1329 [context movedToNewScreen];
1330 }
1331 }
1332
1333#endif // SDL_VIDEO_OPENGL
1334}
1335
1336- (void)windowWillEnterFullScreen:(NSNotification *)aNotification
1337{
1338 SDL_Window *window = _data.window;
1339 const NSUInteger flags = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | NSWindowStyleMaskTitled;
1340
1341 /* For some reason, the fullscreen window won't get any mouse button events
1342 * without the NSWindowStyleMaskTitled flag being set when entering fullscreen,
1343 * so it's needed even if the window is borderless.
1344 */
1345 SetWindowStyle(window, flags);
1346
1347 _data.was_zoomed = !!(window->flags & SDL_WINDOW_MAXIMIZED);
1348
1349 isFullscreenSpace = YES;
1350 inFullscreenTransition = YES;
1351}
1352
1353- (void)windowDidFailToEnterFullScreen:(NSNotification *)aNotification
1354{
1355 SDL_Window *window = _data.window;
1356
1357 if (window->is_destroying) {
1358 return;
1359 }
1360
1361 SetWindowStyle(window, GetWindowStyle(window));
1362
1363 [self clearPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN];
1364 isFullscreenSpace = NO;
1365 inFullscreenTransition = NO;
1366
1367 [self windowDidExitFullScreen:nil];
1368}
1369
1370- (void)windowDidEnterFullScreen:(NSNotification *)aNotification
1371{
1372 SDL_Window *window = _data.window;
1373
1374 inFullscreenTransition = NO;
1375 [self clearPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN];
1376
1377 if ([self windowOperationIsPending:PENDING_OPERATION_LEAVE_FULLSCREEN]) {
1378 [self setFullscreenSpace:NO];
1379 } else {
1380 Cocoa_ToggleFullscreenSpaceMenuVisibility(window);
1381
1382 /* Don't recurse back into UpdateFullscreenMode() if this was hit in
1383 * a blocking transition, as the caller is already waiting in
1384 * UpdateFullscreenMode().
1385 */
1386 if (!_data.in_blocking_transition) {
1387 SDL_UpdateFullscreenMode(window, true, false);
1388 }
1389 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_ENTER_FULLSCREEN, 0, 0);
1390
1391 _data.pending_position = NO;
1392 _data.pending_size = NO;
1393
1394 /* Force the size change event in case it was delivered earlier
1395 while the window was still animating into place.
1396 */
1397 window->w = 0;
1398 window->h = 0;
1399 [self windowDidMove:aNotification];
1400 [self windowDidResize:aNotification];
1401
1402 Cocoa_UpdateClipCursor(window);
1403 }
1404}
1405
1406- (void)windowWillExitFullScreen:(NSNotification *)aNotification
1407{
1408 SDL_Window *window = _data.window;
1409
1410 /* If the windowed mode borders were toggled on while in a fullscreen space,
1411 * NSWindowStyleMaskTitled has to be cleared here, or the window can end up
1412 * in a weird, semi-decorated state upon returning to windowed mode.
1413 */
1414 if (_data.border_toggled && !(window->flags & SDL_WINDOW_BORDERLESS)) {
1415 const NSUInteger flags = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
1416
1417 SetWindowStyle(window, flags);
1418 _data.border_toggled = false;
1419 }
1420
1421 isFullscreenSpace = NO;
1422 inFullscreenTransition = YES;
1423}
1424
1425- (void)windowDidFailToExitFullScreen:(NSNotification *)aNotification
1426{
1427 SDL_Window *window = _data.window;
1428 const NSUInteger flags = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
1429
1430 if (window->is_destroying) {
1431 return;
1432 }
1433
1434 _data.pending_position = NO;
1435 _data.pending_size = NO;
1436 window->last_position_pending = false;
1437 window->last_size_pending = false;
1438
1439 SetWindowStyle(window, flags);
1440
1441 isFullscreenSpace = YES;
1442 inFullscreenTransition = NO;
1443
1444 [self windowDidEnterFullScreen:nil];
1445}
1446
1447- (void)windowDidExitFullScreen:(NSNotification *)aNotification
1448{
1449 SDL_Window *window = _data.window;
1450 NSWindow *nswindow = _data.nswindow;
1451
1452 inFullscreenTransition = NO;
1453 _data.fullscreen_space_requested = NO;
1454
1455 /* As of macOS 10.15, the window decorations can go missing sometimes after
1456 certain fullscreen-desktop->exlusive-fullscreen->windowed mode flows
1457 sometimes. Making sure the style mask always uses the windowed mode style
1458 when returning to windowed mode from a space (instead of using a pending
1459 fullscreen mode style mask) seems to work around that issue.
1460 */
1461 SetWindowStyle(window, GetWindowWindowedStyle(window));
1462
1463 /* Don't recurse back into UpdateFullscreenMode() if this was hit in
1464 * a blocking transition, as the caller is already waiting in
1465 * UpdateFullscreenMode().
1466 */
1467 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0);
1468 if (!_data.in_blocking_transition) {
1469 SDL_UpdateFullscreenMode(window, false, false);
1470 }
1471
1472 if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) {
1473 [nswindow setLevel:NSFloatingWindowLevel];
1474 } else {
1475 [nswindow setLevel:kCGNormalWindowLevel];
1476 }
1477
1478 [self clearPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN];
1479
1480 if ([self windowOperationIsPending:PENDING_OPERATION_ENTER_FULLSCREEN]) {
1481 [self setFullscreenSpace:YES];
1482 } else if ([self windowOperationIsPending:PENDING_OPERATION_MINIMIZE]) {
1483 /* There's some state that isn't quite back to normal when
1484 * windowDidExitFullScreen triggers. For example, the minimize button on
1485 * the title bar doesn't actually enable for another 200 milliseconds or
1486 * so on this MacBook. Camp here and wait for that to happen before
1487 * going on, in case we're exiting fullscreen to minimize, which need
1488 * that window state to be normal before it will work.
1489 */
1490 Cocoa_WaitForMiniaturizable(_data.window);
1491 [self addPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN];
1492 [nswindow miniaturize:nil];
1493 } else {
1494 // Adjust the fullscreen toggle button and readd menu now that we're here.
1495 if (window->flags & SDL_WINDOW_RESIZABLE) {
1496 // resizable windows are Spaces-friendly: they get the "go fullscreen" toggle button on their titlebar.
1497 [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
1498 } else {
1499 [nswindow setCollectionBehavior:NSWindowCollectionBehaviorManaged];
1500 }
1501 [NSMenu setMenuBarVisible:YES];
1502
1503 // Toggle zoom, if changed while fullscreen.
1504 if ([self windowOperationIsPending:PENDING_OPERATION_ZOOM]) {
1505 [self clearPendingWindowOperation:PENDING_OPERATION_ZOOM];
1506 [nswindow zoom:nil];
1507 }
1508
1509 if (![nswindow isZoomed]) {
1510 // Apply a pending window size, if not zoomed.
1511 NSRect rect;
1512 rect.origin.x = _data.pending_position ? window->pending.x : window->floating.x;
1513 rect.origin.y = _data.pending_position ? window->pending.y : window->floating.y;
1514 rect.size.width = _data.pending_size ? window->pending.w : window->floating.w;
1515 rect.size.height = _data.pending_size ? window->pending.h : window->floating.h;
1516 ConvertNSRect(&rect);
1517
1518 if (_data.pending_size) {
1519 [nswindow setContentSize:rect.size];
1520 }
1521 if (_data.pending_position) {
1522 [nswindow setFrameOrigin:rect.origin];
1523 }
1524 }
1525
1526 _data.pending_size = NO;
1527 _data.pending_position = NO;
1528 _data.was_zoomed = NO;
1529
1530 /* Force the size change event in case it was delivered earlier
1531 * while the window was still animating into place.
1532 */
1533 window->w = 0;
1534 window->h = 0;
1535 [self windowDidMove:aNotification];
1536 [self windowDidResize:aNotification];
1537
1538 // FIXME: Why does the window get hidden?
1539 if (!(window->flags & SDL_WINDOW_HIDDEN)) {
1540 Cocoa_ShowWindow(SDL_GetVideoDevice(), window);
1541 }
1542
1543 Cocoa_UpdateClipCursor(window);
1544 }
1545}
1546
1547- (NSApplicationPresentationOptions)window:(NSWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions
1548{
1549 if (_data.window->fullscreen_exclusive) {
1550 return NSApplicationPresentationFullScreen | NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
1551 } else {
1552 return proposedOptions;
1553 }
1554}
1555
1556/* We'll respond to key events by mostly doing nothing so we don't beep.
1557 * We could handle key messages here, but we lose some in the NSApp dispatch,
1558 * where they get converted to action messages, etc.
1559 */
1560- (void)flagsChanged:(NSEvent *)theEvent
1561{
1562 // Cocoa_HandleKeyEvent(SDL_GetVideoDevice(), theEvent);
1563
1564 /* Catch capslock in here as a special case:
1565 https://developer.apple.com/library/archive/qa/qa1519/_index.html
1566 Note that technote's check of keyCode doesn't work. At least on the
1567 10.15 beta, capslock comes through here as keycode 255, but it's safe
1568 to send duplicate key events; SDL filters them out quickly in
1569 SDL_SendKeyboardKey(). */
1570
1571 /* Also note that SDL_SendKeyboardKey expects all capslock events to be
1572 keypresses; it won't toggle the mod state if you send a keyrelease. */
1573 const bool osenabled = ([theEvent modifierFlags] & NSEventModifierFlagCapsLock) ? true : false;
1574 const bool sdlenabled = (SDL_GetModState() & SDL_KMOD_CAPS) ? true : false;
1575 if (osenabled ^ sdlenabled) {
1576 SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_CAPSLOCK, true);
1577 SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_CAPSLOCK, false);
1578 }
1579}
1580- (void)keyDown:(NSEvent *)theEvent
1581{
1582 // Cocoa_HandleKeyEvent(SDL_GetVideoDevice(), theEvent);
1583}
1584- (void)keyUp:(NSEvent *)theEvent
1585{
1586 // Cocoa_HandleKeyEvent(SDL_GetVideoDevice(), theEvent);
1587}
1588
1589/* We'll respond to selectors by doing nothing so we don't beep.
1590 * The escape key gets converted to a "cancel" selector, etc.
1591 */
1592- (void)doCommandBySelector:(SEL)aSelector
1593{
1594 // NSLog(@"doCommandBySelector: %@\n", NSStringFromSelector(aSelector));
1595}
1596
1597- (void)updateHitTest
1598{
1599 SDL_Window *window = _data.window;
1600 BOOL draggable = NO;
1601
1602 if (window->hit_test) {
1603 float x, y;
1604 SDL_Point point;
1605
1606 SDL_GetGlobalMouseState(&x, &y);
1607 point.x = (int)SDL_roundf(x - window->x);
1608 point.y = (int)SDL_roundf(y - window->y);
1609 if (point.x >= 0 && point.x < window->w && point.y >= 0 && point.y < window->h) {
1610 if (window->hit_test(window, &point, window->hit_test_data) == SDL_HITTEST_DRAGGABLE) {
1611 draggable = YES;
1612 }
1613 }
1614 }
1615
1616 if (isDragAreaRunning != draggable) {
1617 isDragAreaRunning = draggable;
1618 [_data.nswindow setMovableByWindowBackground:draggable];
1619 }
1620}
1621
1622- (BOOL)processHitTest:(NSEvent *)theEvent
1623{
1624 SDL_Window *window = _data.window;
1625
1626 if (window->hit_test) { // if no hit-test, skip this.
1627 const NSPoint location = [theEvent locationInWindow];
1628 const SDL_Point point = { (int)location.x, window->h - (((int)location.y) - 1) };
1629 const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data);
1630 if (rc == SDL_HITTEST_DRAGGABLE) {
1631 if (!isDragAreaRunning) {
1632 isDragAreaRunning = YES;
1633 [_data.nswindow setMovableByWindowBackground:YES];
1634 }
1635 return YES; // dragging!
1636 } else {
1637 if (isDragAreaRunning) {
1638 isDragAreaRunning = NO;
1639 [_data.nswindow setMovableByWindowBackground:NO];
1640 return YES; // was dragging, drop event.
1641 }
1642 }
1643 }
1644
1645 return NO; // not a special area, carry on.
1646}
1647
1648static void Cocoa_SendMouseButtonClicks(SDL_Mouse *mouse, NSEvent *theEvent, SDL_Window *window, Uint8 button, bool down)
1649{
1650 SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
1651 //const int clicks = (int)[theEvent clickCount];
1652 SDL_Window *focus = SDL_GetKeyboardFocus();
1653
1654 // macOS will send non-left clicks to background windows without raising them, so we need to
1655 // temporarily adjust the mouse position when this happens, as `mouse` will be tracking
1656 // the position in the currently-focused window. We don't (currently) send a mousemove
1657 // event for the background window, this just makes sure the button is reported at the
1658 // correct position in its own event.
1659 if (focus && ([theEvent window] == ((__bridge SDL_CocoaWindowData *)focus->internal).nswindow)) {
1660 //SDL_SendMouseButtonClicks(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down, clicks);
1661 SDL_SendMouseButton(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down);
1662 } else {
1663 const float orig_x = mouse->x;
1664 const float orig_y = mouse->y;
1665 const NSPoint point = [theEvent locationInWindow];
1666 mouse->x = (int)point.x;
1667 mouse->y = (int)(window->h - point.y);
1668 //SDL_SendMouseButtonClicks(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down, clicks);
1669 SDL_SendMouseButton(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down);
1670 mouse->x = orig_x;
1671 mouse->y = orig_y;
1672 }
1673}
1674
1675- (void)mouseDown:(NSEvent *)theEvent
1676{
1677 if (Cocoa_HandlePenEvent(_data, theEvent)) {
1678 return; // pen code handled it.
1679 }
1680
1681 SDL_Mouse *mouse = SDL_GetMouse();
1682 int button;
1683
1684 if (!mouse) {
1685 return;
1686 }
1687
1688 // Ignore events that aren't inside the client area (i.e. title bar.)
1689 if ([theEvent window]) {
1690 NSRect windowRect = [[[theEvent window] contentView] frame];
1691 if (!NSMouseInRect([theEvent locationInWindow], windowRect, NO)) {
1692 return;
1693 }
1694 }
1695
1696 switch ([theEvent buttonNumber]) {
1697 case 0:
1698 if (([theEvent modifierFlags] & NSEventModifierFlagControl) &&
1699 GetHintCtrlClickEmulateRightClick()) {
1700 wasCtrlLeft = YES;
1701 button = SDL_BUTTON_RIGHT;
1702 } else {
1703 wasCtrlLeft = NO;
1704 button = SDL_BUTTON_LEFT;
1705 }
1706 break;
1707 case 1:
1708 button = SDL_BUTTON_RIGHT;
1709 break;
1710 case 2:
1711 button = SDL_BUTTON_MIDDLE;
1712 break;
1713 default:
1714 button = (int)[theEvent buttonNumber] + 1;
1715 break;
1716 }
1717
1718 if (button == SDL_BUTTON_LEFT && [self processHitTest:theEvent]) {
1719 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0);
1720 return; // dragging, drop event.
1721 }
1722
1723 Cocoa_SendMouseButtonClicks(mouse, theEvent, _data.window, button, true);
1724}
1725
1726- (void)rightMouseDown:(NSEvent *)theEvent
1727{
1728 [self mouseDown:theEvent];
1729}
1730
1731- (void)otherMouseDown:(NSEvent *)theEvent
1732{
1733 [self mouseDown:theEvent];
1734}
1735
1736- (void)mouseUp:(NSEvent *)theEvent
1737{
1738 if (Cocoa_HandlePenEvent(_data, theEvent)) {
1739 return; // pen code handled it.
1740 }
1741
1742 SDL_Mouse *mouse = SDL_GetMouse();
1743 int button;
1744
1745 if (!mouse) {
1746 return;
1747 }
1748
1749 switch ([theEvent buttonNumber]) {
1750 case 0:
1751 if (wasCtrlLeft) {
1752 button = SDL_BUTTON_RIGHT;
1753 wasCtrlLeft = NO;
1754 } else {
1755 button = SDL_BUTTON_LEFT;
1756 }
1757 break;
1758 case 1:
1759 button = SDL_BUTTON_RIGHT;
1760 break;
1761 case 2:
1762 button = SDL_BUTTON_MIDDLE;
1763 break;
1764 default:
1765 button = (int)[theEvent buttonNumber] + 1;
1766 break;
1767 }
1768
1769 if (button == SDL_BUTTON_LEFT && [self processHitTest:theEvent]) {
1770 SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0);
1771 return; // stopped dragging, drop event.
1772 }
1773
1774 Cocoa_SendMouseButtonClicks(mouse, theEvent, _data.window, button, false);
1775}
1776
1777- (void)rightMouseUp:(NSEvent *)theEvent
1778{
1779 [self mouseUp:theEvent];
1780}
1781
1782- (void)otherMouseUp:(NSEvent *)theEvent
1783{
1784 [self mouseUp:theEvent];
1785}
1786
1787- (void)mouseMoved:(NSEvent *)theEvent
1788{
1789 if (Cocoa_HandlePenEvent(_data, theEvent)) {
1790 return; // pen code handled it.
1791 }
1792
1793 SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
1794 SDL_Mouse *mouse = SDL_GetMouse();
1795 NSPoint point;
1796 float x, y;
1797 SDL_Window *window;
1798 NSView *contentView;
1799
1800 if (!mouse) {
1801 return;
1802 }
1803
1804 if (!Cocoa_GetMouseFocus()) {
1805 // The mouse is no longer over any window in the application
1806 SDL_SetMouseFocus(NULL);
1807 return;
1808 }
1809
1810 window = _data.window;
1811 contentView = _data.sdlContentView;
1812 point = [theEvent locationInWindow];
1813
1814 if ([contentView mouse:[contentView convertPoint:point fromView:nil] inRect:[contentView bounds]] &&
1815 [NSCursor currentCursor] != Cocoa_GetDesiredCursor()) {
1816 // The wrong cursor is on screen, fix it. This fixes an macOS bug that is only known to
1817 // occur in fullscreen windows on the built-in displays of newer MacBooks with camera
1818 // notches. When the mouse is moved near the top of such a window (within about 44 units)
1819 // and then moved back down, the cursor rects aren't respected.
1820 [_data.nswindow invalidateCursorRectsForView:contentView];
1821 }
1822
1823 if (window->flags & SDL_WINDOW_TRANSPARENT) {
1824 [self updateIgnoreMouseState:theEvent];
1825 }
1826
1827 if ([self processHitTest:theEvent]) {
1828 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0);
1829 return; // dragging, drop event.
1830 }
1831
1832 if (mouse->relative_mode) {
1833 return;
1834 }
1835
1836 x = point.x;
1837 y = (window->h - point.y);
1838
1839 if (NSAppKitVersionNumber >= NSAppKitVersionNumber10_13_2) {
1840 // Mouse grab is taken care of by the confinement rect
1841 } else {
1842 CGPoint cgpoint;
1843 if (ShouldAdjustCoordinatesForGrab(window) &&
1844 AdjustCoordinatesForGrab(window, window->x + x, window->y + y, &cgpoint)) {
1845 Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
1846 CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
1847 CGAssociateMouseAndMouseCursorPosition(YES);
1848 }
1849 }
1850
1851 SDL_SendMouseMotion(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, false, x, y);
1852}
1853
1854- (void)mouseDragged:(NSEvent *)theEvent
1855{
1856 [self mouseMoved:theEvent];
1857}
1858
1859- (void)rightMouseDragged:(NSEvent *)theEvent
1860{
1861 [self mouseMoved:theEvent];
1862}
1863
1864- (void)otherMouseDragged:(NSEvent *)theEvent
1865{
1866 [self mouseMoved:theEvent];
1867}
1868
1869- (void)scrollWheel:(NSEvent *)theEvent
1870{
1871 Cocoa_HandleMouseWheel(_data.window, theEvent);
1872}
1873
1874- (BOOL)isTouchFromTrackpad:(NSEvent *)theEvent
1875{
1876 SDL_Window *window = _data.window;
1877 SDL_CocoaVideoData *videodata = ((__bridge SDL_CocoaWindowData *)window->internal).videodata;
1878
1879 /* if this a MacBook trackpad, we'll make input look like a synthesized
1880 event. This is backwards from reality, but better matches user
1881 expectations. You can make it look like a generic touch device instead
1882 with the SDL_HINT_TRACKPAD_IS_TOUCH_ONLY hint. */
1883 BOOL istrackpad = NO;
1884 if (!videodata.trackpad_is_touch_only) {
1885 @try {
1886 istrackpad = ([theEvent subtype] == NSEventSubtypeMouseEvent);
1887 }
1888 @catch (NSException *e) {
1889 /* if NSEvent type doesn't have subtype, such as NSEventTypeBeginGesture on
1890 * macOS 10.5 to 10.10, then NSInternalInconsistencyException is thrown.
1891 * This still prints a message to terminal so catching it's not an ideal solution.
1892 *
1893 * *** Assertion failure in -[NSEvent subtype]
1894 */
1895 }
1896 }
1897 return istrackpad;
1898}
1899
1900- (void)touchesBeganWithEvent:(NSEvent *)theEvent
1901{
1902 NSSet *touches;
1903 SDL_TouchID touchID;
1904 int existingTouchCount;
1905 const BOOL istrackpad = [self isTouchFromTrackpad:theEvent];
1906
1907 touches = [theEvent touchesMatchingPhase:NSTouchPhaseAny inView:nil];
1908 touchID = istrackpad ? SDL_MOUSE_TOUCHID : (SDL_TouchID)(intptr_t)[[touches anyObject] device];
1909 existingTouchCount = 0;
1910
1911 for (NSTouch *touch in touches) {
1912 if ([touch phase] != NSTouchPhaseBegan) {
1913 existingTouchCount++;
1914 }
1915 }
1916 if (existingTouchCount == 0) {
1917 int numFingers;
1918 SDL_Finger **fingers = SDL_GetTouchFingers(touchID, &numFingers);
1919 if (fingers) {
1920 DLog("Reset Lost Fingers: %d", numFingers);
1921 for (--numFingers; numFingers >= 0; --numFingers) {
1922 const SDL_Finger *finger = fingers[numFingers];
1923 /* trackpad touches have no window. If we really wanted one we could
1924 * use the window that has mouse or keyboard focus.
1925 * Sending a null window currently also prevents synthetic mouse
1926 * events from being generated from touch events.
1927 */
1928 SDL_Window *window = NULL;
1929 SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchID, finger->id, window, SDL_EVENT_FINGER_CANCELED, 0, 0, 0);
1930 }
1931 SDL_free(fingers);
1932 }
1933 }
1934
1935 DLog("Began Fingers: %lu .. existing: %d", (unsigned long)[touches count], existingTouchCount);
1936 [self handleTouches:NSTouchPhaseBegan withEvent:theEvent];
1937}
1938
1939- (void)touchesMovedWithEvent:(NSEvent *)theEvent
1940{
1941 [self handleTouches:NSTouchPhaseMoved withEvent:theEvent];
1942}
1943
1944- (void)touchesEndedWithEvent:(NSEvent *)theEvent
1945{
1946 [self handleTouches:NSTouchPhaseEnded withEvent:theEvent];
1947}
1948
1949- (void)touchesCancelledWithEvent:(NSEvent *)theEvent
1950{
1951 [self handleTouches:NSTouchPhaseCancelled withEvent:theEvent];
1952}
1953
1954- (void)handleTouches:(NSTouchPhase)phase withEvent:(NSEvent *)theEvent
1955{
1956 NSSet *touches = [theEvent touchesMatchingPhase:phase inView:nil];
1957 const BOOL istrackpad = [self isTouchFromTrackpad:theEvent];
1958 SDL_FingerID fingerId;
1959 float x, y;
1960
1961 for (NSTouch *touch in touches) {
1962 const SDL_TouchID touchId = istrackpad ? SDL_MOUSE_TOUCHID : (SDL_TouchID)(uintptr_t)[touch device];
1963 SDL_TouchDeviceType devtype = SDL_TOUCH_DEVICE_INDIRECT_ABSOLUTE;
1964
1965 /* trackpad touches have no window. If we really wanted one we could
1966 * use the window that has mouse or keyboard focus.
1967 * Sending a null window currently also prevents synthetic mouse events
1968 * from being generated from touch events.
1969 */
1970 SDL_Window *window = NULL;
1971
1972 /* TODO: Before implementing direct touch support here, we need to
1973 * figure out whether the OS generates mouse events from them on its
1974 * own. If it does, we should prevent SendTouch from generating
1975 * synthetic mouse events for these touches itself (while also
1976 * sending a window.) It will also need to use normalized window-
1977 * relative coordinates via [touch locationInView:].
1978 */
1979 if ([touch type] == NSTouchTypeDirect) {
1980 continue;
1981 }
1982
1983 if (SDL_AddTouch(touchId, devtype, "") < 0) {
1984 return;
1985 }
1986
1987 fingerId = (SDL_FingerID)(uintptr_t)[touch identity];
1988 x = [touch normalizedPosition].x;
1989 y = [touch normalizedPosition].y;
1990 // Make the origin the upper left instead of the lower left
1991 y = 1.0f - y;
1992
1993 switch (phase) {
1994 case NSTouchPhaseBegan:
1995 SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, SDL_EVENT_FINGER_DOWN, x, y, 1.0f);
1996 break;
1997 case NSTouchPhaseEnded:
1998 SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, SDL_EVENT_FINGER_UP, x, y, 1.0f);
1999 break;
2000 case NSTouchPhaseCancelled:
2001 SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, SDL_EVENT_FINGER_CANCELED, x, y, 1.0f);
2002 break;
2003 case NSTouchPhaseMoved:
2004 SDL_SendTouchMotion(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, x, y, 1.0f);
2005 break;
2006 default:
2007 break;
2008 }
2009 }
2010}
2011
2012- (void)tabletProximity:(NSEvent *)theEvent
2013{
2014 Cocoa_HandlePenEvent(_data, theEvent);
2015}
2016
2017- (void)tabletPoint:(NSEvent *)theEvent
2018{
2019 Cocoa_HandlePenEvent(_data, theEvent);
2020}
2021
2022@end
2023
2024@interface SDL3View : NSView
2025{
2026 SDL_Window *_sdlWindow;
2027}
2028
2029- (void)setSDLWindow:(SDL_Window *)window;
2030
2031// The default implementation doesn't pass rightMouseDown to responder chain
2032- (void)rightMouseDown:(NSEvent *)theEvent;
2033- (BOOL)mouseDownCanMoveWindow;
2034- (void)drawRect:(NSRect)dirtyRect;
2035- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent;
2036- (BOOL)wantsUpdateLayer;
2037- (void)updateLayer;
2038@end
2039
2040@implementation SDL3View
2041
2042- (void)setSDLWindow:(SDL_Window *)window
2043{
2044 _sdlWindow = window;
2045}
2046
2047/* this is used on older macOS revisions, and newer ones which emulate old
2048 NSOpenGLContext behaviour while still using a layer under the hood. 10.8 and
2049 later use updateLayer, up until 10.14.2 or so, which uses drawRect without
2050 a GraphicsContext and with a layer active instead (for OpenGL contexts). */
2051- (void)drawRect:(NSRect)dirtyRect
2052{
2053 /* Force the graphics context to clear to black so we don't get a flash of
2054 white until the app is ready to draw. In practice on modern macOS, this
2055 only gets called for window creation and other extraordinary events. */
2056 BOOL transparent = (_sdlWindow->flags & SDL_WINDOW_TRANSPARENT) != 0;
2057 if ([NSGraphicsContext currentContext]) {
2058 NSColor *fillColor = transparent ? [NSColor clearColor] : [NSColor blackColor];
2059 [fillColor setFill];
2060 NSRectFill(dirtyRect);
2061 } else if (self.layer) {
2062 CFStringRef color = transparent ? kCGColorClear : kCGColorBlack;
2063 self.layer.backgroundColor = CGColorGetConstantColor(color);
2064 }
2065
2066 Cocoa_SendExposedEventIfVisible(_sdlWindow);
2067}
2068
2069- (BOOL)wantsUpdateLayer
2070{
2071 return YES;
2072}
2073
2074// This is also called when a Metal layer is active.
2075- (void)updateLayer
2076{
2077 /* Force the graphics context to clear to black so we don't get a flash of
2078 white until the app is ready to draw. In practice on modern macOS, this
2079 only gets called for window creation and other extraordinary events. */
2080 BOOL transparent = (_sdlWindow->flags & SDL_WINDOW_TRANSPARENT) != 0;
2081 CFStringRef color = transparent ? kCGColorClear : kCGColorBlack;
2082 self.layer.backgroundColor = CGColorGetConstantColor(color);
2083 ScheduleContextUpdates((__bridge SDL_CocoaWindowData *)_sdlWindow->internal);
2084 Cocoa_SendExposedEventIfVisible(_sdlWindow);
2085}
2086
2087- (void)rightMouseDown:(NSEvent *)theEvent
2088{
2089 [[self nextResponder] rightMouseDown:theEvent];
2090}
2091
2092- (BOOL)mouseDownCanMoveWindow
2093{
2094 /* Always say YES, but this doesn't do anything until we call
2095 -[NSWindow setMovableByWindowBackground:YES], which we ninja-toggle
2096 during mouse events when we're using a drag area. */
2097 return YES;
2098}
2099
2100- (void)resetCursorRects
2101{
2102 [super resetCursorRects];
2103 [self addCursorRect:[self bounds]
2104 cursor:Cocoa_GetDesiredCursor()];
2105}
2106
2107- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
2108{
2109 if (SDL_GetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH)) {
2110 return SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, false);
2111 } else {
2112 return SDL_GetHintBoolean("SDL_MAC_MOUSE_FOCUS_CLICKTHROUGH", false);
2113 }
2114}
2115
2116@end
2117
2118static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, NSWindow *nswindow, NSView *nsview)
2119{
2120 @autoreleasepool {
2121 SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal;
2122 SDL_CocoaWindowData *data;
2123
2124 // Allocate the window data
2125 data = [[SDL_CocoaWindowData alloc] init];
2126 if (!data) {
2127 return SDL_OutOfMemory();
2128 }
2129 data.window = window;
2130 data.nswindow = nswindow;
2131 data.videodata = videodata;
2132 data.window_number = nswindow.windowNumber;
2133 data.nscontexts = [[NSMutableArray alloc] init];
2134 data.sdlContentView = nsview;
2135
2136 // Create an event listener for the window
2137 data.listener = [[SDL3Cocoa_WindowListener alloc] init];
2138
2139 // Fill in the SDL window with the window data
2140 {
2141 int x, y;
2142 NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]];
2143 ConvertNSRect(&rect);
2144 SDL_GlobalToRelativeForWindow(window, (int)rect.origin.x, (int)rect.origin.y, &x, &y);
2145 window->x = x;
2146 window->y = y;
2147 window->w = (int)rect.size.width;
2148 window->h = (int)rect.size.height;
2149 }
2150
2151 // Set up the listener after we create the view
2152 [data.listener listen:data];
2153
2154 if ([nswindow isVisible]) {
2155 window->flags &= ~SDL_WINDOW_HIDDEN;
2156 } else {
2157 window->flags |= SDL_WINDOW_HIDDEN;
2158 }
2159
2160 {
2161 unsigned long style = [nswindow styleMask];
2162
2163 /* NSWindowStyleMaskBorderless is zero, and it's possible to be
2164 Resizeable _and_ borderless, so we can't do a simple bitwise AND
2165 of NSWindowStyleMaskBorderless here. */
2166 if ((style & ~(NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable)) == NSWindowStyleMaskBorderless) {
2167 window->flags |= SDL_WINDOW_BORDERLESS;
2168 } else {
2169 window->flags &= ~SDL_WINDOW_BORDERLESS;
2170 }
2171 if (style & NSWindowStyleMaskResizable) {
2172 window->flags |= SDL_WINDOW_RESIZABLE;
2173 } else {
2174 window->flags &= ~SDL_WINDOW_RESIZABLE;
2175 }
2176 }
2177
2178 // isZoomed always returns true if the window is not resizable
2179 if ((window->flags & SDL_WINDOW_RESIZABLE) && [nswindow isZoomed]) {
2180 window->flags |= SDL_WINDOW_MAXIMIZED;
2181 } else {
2182 window->flags &= ~SDL_WINDOW_MAXIMIZED;
2183 }
2184
2185 if ([nswindow isMiniaturized]) {
2186 window->flags |= SDL_WINDOW_MINIMIZED;
2187 } else {
2188 window->flags &= ~SDL_WINDOW_MINIMIZED;
2189 }
2190
2191 if (window->parent) {
2192 NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow;
2193 [nsparent addChildWindow:nswindow ordered:NSWindowAbove];
2194
2195 /* FIXME: Should not need to call addChildWindow then orderOut.
2196 Attaching a hidden child window to a hidden parent window will cause the child window
2197 to show when the parent does. We therefore shouldn't attach the child window here as we're
2198 going to do so when the child window is explicitly shown later but skipping the addChildWindow
2199 entirely causes the child window to not get key focus correctly the first time it's shown. Adding
2200 then immediately ordering out (removing) the window does work. */
2201 if (window->flags & SDL_WINDOW_HIDDEN) {
2202 [nswindow orderOut:nil];
2203 }
2204 }
2205
2206 if (!SDL_WINDOW_IS_POPUP(window)) {
2207 if ([nswindow isKeyWindow]) {
2208 window->flags |= SDL_WINDOW_INPUT_FOCUS;
2209 Cocoa_SetKeyboardFocus(data.window, true);
2210 }
2211 } else {
2212 if (window->flags & SDL_WINDOW_TOOLTIP) {
2213 [nswindow setIgnoresMouseEvents:YES];
2214 } else if (window->flags & SDL_WINDOW_POPUP_MENU) {
2215 Cocoa_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus());
2216 }
2217 }
2218
2219 if (nswindow.isOpaque) {
2220 window->flags &= ~SDL_WINDOW_TRANSPARENT;
2221 } else {
2222 window->flags |= SDL_WINDOW_TRANSPARENT;
2223 }
2224
2225 /* SDL_CocoaWindowData will be holding a strong reference to the NSWindow, and
2226 * it will also call [NSWindow close] in DestroyWindow before releasing the
2227 * NSWindow, so the extra release provided by releasedWhenClosed isn't
2228 * necessary. */
2229 nswindow.releasedWhenClosed = NO;
2230
2231 /* Prevents the window's "window device" from being destroyed when it is
2232 * hidden. See http://www.mikeash.com/pyblog/nsopenglcontext-and-one-shot.html
2233 */
2234 [nswindow setOneShot:NO];
2235
2236 if (window->flags & SDL_WINDOW_EXTERNAL) {
2237 // Query the title from the existing window
2238 NSString *title = [nswindow title];
2239 if (title) {
2240 window->title = SDL_strdup([title UTF8String]);
2241 }
2242 }
2243
2244 SDL_PropertiesID props = SDL_GetWindowProperties(window);
2245 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, (__bridge void *)data.nswindow);
2246 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_COCOA_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG);
2247
2248 // All done!
2249 window->internal = (SDL_WindowData *)CFBridgingRetain(data);
2250 return true;
2251 }
2252}
2253
2254bool Cocoa_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props)
2255{
2256 @autoreleasepool {
2257 SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal;
2258 const void *data = SDL_GetPointerProperty(create_props, "sdl2-compat.external_window", NULL);
2259 NSWindow *nswindow = nil;
2260 NSView *nsview = nil;
2261
2262 if (data) {
2263 if ([(__bridge id)data isKindOfClass:[NSWindow class]]) {
2264 nswindow = (__bridge NSWindow *)data;
2265 } else if ([(__bridge id)data isKindOfClass:[NSView class]]) {
2266 nsview = (__bridge NSView *)data;
2267 } else {
2268 SDL_assert(false);
2269 }
2270 } else {
2271 nswindow = (__bridge NSWindow *)SDL_GetPointerProperty(create_props, SDL_PROP_WINDOW_CREATE_COCOA_WINDOW_POINTER, NULL);
2272 nsview = (__bridge NSView *)SDL_GetPointerProperty(create_props, SDL_PROP_WINDOW_CREATE_COCOA_VIEW_POINTER, NULL);
2273 }
2274 if (nswindow && !nsview) {
2275 nsview = [nswindow contentView];
2276 }
2277 if (nsview && !nswindow) {
2278 nswindow = [nsview window];
2279 }
2280 if (nswindow) {
2281 window->flags |= SDL_WINDOW_EXTERNAL;
2282 } else {
2283 int x, y;
2284 NSScreen *screen;
2285 NSRect rect, screenRect;
2286 NSUInteger style;
2287 SDL3View *contentView;
2288
2289 SDL_RelativeToGlobalForWindow(window, window->x, window->y, &x, &y);
2290 rect.origin.x = x;
2291 rect.origin.y = y;
2292 rect.size.width = window->w;
2293 rect.size.height = window->h;
2294 ConvertNSRect(&rect);
2295
2296 style = GetWindowStyle(window);
2297
2298 // Figure out which screen to place this window
2299 screen = ScreenForRect(&rect);
2300 screenRect = [screen frame];
2301 rect.origin.x -= screenRect.origin.x;
2302 rect.origin.y -= screenRect.origin.y;
2303
2304 // Constrain the popup
2305 if (SDL_WINDOW_IS_POPUP(window)) {
2306 if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) {
2307 rect.origin.x -= (rect.origin.x + rect.size.width) - (screenRect.origin.x + screenRect.size.width);
2308 }
2309 if (rect.origin.y + rect.size.height > screenRect.origin.y + screenRect.size.height) {
2310 rect.origin.y -= (rect.origin.y + rect.size.height) - (screenRect.origin.y + screenRect.size.height);
2311 }
2312 rect.origin.x = SDL_max(rect.origin.x, screenRect.origin.x);
2313 rect.origin.y = SDL_max(rect.origin.y, screenRect.origin.y);
2314 }
2315
2316 @try {
2317 nswindow = [[SDL3Window alloc] initWithContentRect:rect styleMask:style backing:NSBackingStoreBuffered defer:NO screen:screen];
2318 }
2319 @catch (NSException *e) {
2320 return SDL_SetError("%s", [[e reason] UTF8String]);
2321 }
2322
2323 [nswindow setColorSpace:[NSColorSpace sRGBColorSpace]];
2324
2325 [nswindow setTabbingMode:NSWindowTabbingModeDisallowed];
2326
2327 if (videodata.allow_spaces) {
2328 // we put fullscreen desktop windows in their own Space, without a toggle button or menubar, later
2329 if (window->flags & SDL_WINDOW_RESIZABLE) {
2330 // resizable windows are Spaces-friendly: they get the "go fullscreen" toggle button on their titlebar.
2331 [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
2332 }
2333 }
2334
2335 // Create a default view for this window
2336 rect = [nswindow contentRectForFrameRect:[nswindow frame]];
2337 contentView = [[SDL3View alloc] initWithFrame:rect];
2338 [contentView setSDLWindow:window];
2339 nsview = contentView;
2340 }
2341
2342 if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) {
2343 [nswindow setLevel:NSFloatingWindowLevel];
2344 }
2345
2346 if (window->flags & SDL_WINDOW_TRANSPARENT) {
2347 nswindow.opaque = NO;
2348 nswindow.hasShadow = NO;
2349 nswindow.backgroundColor = [NSColor clearColor];
2350 }
2351
2352// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it.
2353#ifdef __clang__
2354#pragma clang diagnostic push
2355#pragma clang diagnostic ignored "-Wdeprecated-declarations"
2356#endif
2357 /* Note: as of the macOS 10.15 SDK, this defaults to YES instead of NO when
2358 * the NSHighResolutionCapable boolean is set in Info.plist. */
2359 BOOL highdpi = (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) ? YES : NO;
2360 [nsview setWantsBestResolutionOpenGLSurface:highdpi];
2361#ifdef __clang__
2362#pragma clang diagnostic pop
2363#endif
2364
2365#ifdef SDL_VIDEO_OPENGL_ES2
2366#ifdef SDL_VIDEO_OPENGL_EGL
2367 if ((window->flags & SDL_WINDOW_OPENGL) &&
2368 _this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_ES) {
2369 [nsview setWantsLayer:TRUE];
2370 if ((window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY)) {
2371 nsview.layer.contentsScale = nswindow.screen.backingScaleFactor;
2372 } else {
2373 nsview.layer.contentsScale = 1;
2374 }
2375 }
2376#endif // SDL_VIDEO_OPENGL_EGL
2377#endif // SDL_VIDEO_OPENGL_ES2
2378 [nswindow setContentView:nsview];
2379
2380 if (!SetupWindowData(_this, window, nswindow, nsview)) {
2381 return false;
2382 }
2383
2384 if (!(window->flags & SDL_WINDOW_OPENGL)) {
2385 return true;
2386 }
2387
2388 // The rest of this macro mess is for OpenGL or OpenGL ES windows
2389#ifdef SDL_VIDEO_OPENGL_ES2
2390 if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_ES) {
2391#ifdef SDL_VIDEO_OPENGL_EGL
2392 if (!Cocoa_GLES_SetupWindow(_this, window)) {
2393 Cocoa_DestroyWindow(_this, window);
2394 return false;
2395 }
2396 return true;
2397#else
2398 return SDL_SetError("Could not create GLES window surface (EGL support not configured)");
2399#endif // SDL_VIDEO_OPENGL_EGL
2400 }
2401#endif // SDL_VIDEO_OPENGL_ES2
2402 return true;
2403 }
2404}
2405
2406void Cocoa_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window)
2407{
2408 @autoreleasepool {
2409 const char *title = window->title ? window->title : "";
2410 NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow;
2411 NSString *string = [[NSString alloc] initWithUTF8String:title];
2412 [nswindow setTitle:string];
2413 }
2414}
2415
2416bool Cocoa_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon)
2417{
2418 @autoreleasepool {
2419 NSImage *nsimage = Cocoa_CreateImage(icon);
2420
2421 if (nsimage) {
2422 [NSApp setApplicationIconImage:nsimage];
2423
2424 return true;
2425 }
2426
2427 return SDL_SetError("Unable to set the window's icon");
2428 }
2429}
2430
2431bool Cocoa_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window)
2432{
2433 @autoreleasepool {
2434 SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal;
2435 NSWindow *nswindow = windata.nswindow;
2436 NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]];
2437 BOOL fullscreen = (window->flags & SDL_WINDOW_FULLSCREEN) ? YES : NO;
2438 int x, y;
2439
2440 if ([windata.listener isInFullscreenSpaceTransition]) {
2441 windata.pending_position = YES;
2442 return true;
2443 }
2444
2445 if (!(window->flags & SDL_WINDOW_MAXIMIZED)) {
2446 if (fullscreen) {
2447 SDL_VideoDisplay *display = SDL_GetVideoDisplayForFullscreenWindow(window);
2448 SDL_Rect r;
2449 SDL_GetDisplayBounds(display->id, &r);
2450
2451 rect.origin.x = r.x;
2452 rect.origin.y = r.y;
2453 } else {
2454 SDL_RelativeToGlobalForWindow(window, window->pending.x, window->pending.y, &x, &y);
2455 rect.origin.x = x;
2456 rect.origin.y = y;
2457 }
2458 ConvertNSRect(&rect);
2459
2460 // Position and constrain the popup
2461 if (SDL_WINDOW_IS_POPUP(window)) {
2462 NSRect screenRect = [ScreenForRect(&rect) frame];
2463
2464 if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) {
2465 rect.origin.x -= (rect.origin.x + rect.size.width) - (screenRect.origin.x + screenRect.size.width);
2466 }
2467 if (rect.origin.y + rect.size.height > screenRect.origin.y + screenRect.size.height) {
2468 rect.origin.y -= (rect.origin.y + rect.size.height) - (screenRect.origin.y + screenRect.size.height);
2469 }
2470 rect.origin.x = SDL_max(rect.origin.x, screenRect.origin.x);
2471 rect.origin.y = SDL_max(rect.origin.y, screenRect.origin.y);
2472 }
2473
2474 [nswindow setFrameOrigin:rect.origin];
2475
2476 ScheduleContextUpdates(windata);
2477 }
2478 }
2479 return true;
2480}
2481
2482void Cocoa_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window)
2483{
2484 @autoreleasepool {
2485 SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal;
2486 NSWindow *nswindow = windata.nswindow;
2487
2488 if ([windata.listener isInFullscreenSpaceTransition]) {
2489 windata.pending_size = YES;
2490 return;
2491 }
2492
2493 if (!Cocoa_IsWindowZoomed(window)) {
2494 int x, y;
2495 NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]];
2496
2497 /* Cocoa will resize the window from the bottom-left rather than the
2498 * top-left when -[nswindow setContentSize:] is used, so we must set the
2499 * entire frame based on the new size, in order to preserve the position.
2500 */
2501 SDL_RelativeToGlobalForWindow(window, window->floating.x, window->floating.y, &x, &y);
2502 rect.origin.x = x;
2503 rect.origin.y = y;
2504 rect.size.width = window->pending.w;
2505 rect.size.height = window->pending.h;
2506 ConvertNSRect(&rect);
2507
2508 [nswindow setFrame:[nswindow frameRectForContentRect:rect] display:YES];
2509 ScheduleContextUpdates(windata);
2510 } else {
2511 // Can't set the window size.
2512 window->last_size_pending = false;
2513 }
2514 }
2515}
2516
2517void Cocoa_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window)
2518{
2519 @autoreleasepool {
2520 SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal;
2521
2522 NSSize minSize;
2523 minSize.width = window->min_w;
2524 minSize.height = window->min_h;
2525
2526 [windata.nswindow setContentMinSize:minSize];
2527 }
2528}
2529
2530void Cocoa_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window)
2531{
2532 @autoreleasepool {
2533 SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal;
2534
2535 NSSize maxSize;
2536 maxSize.width = window->max_w;
2537 maxSize.height = window->max_h;
2538
2539 [windata.nswindow setContentMaxSize:maxSize];
2540 }
2541}
2542
2543void Cocoa_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window)
2544{
2545 @autoreleasepool {
2546 SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal;
2547
2548 if (window->min_aspect > 0.0f && window->min_aspect == window->max_aspect) {
2549 int numerator = 0, denominator = 1;
2550 SDL_CalculateFraction(window->max_aspect, &numerator, &denominator);
2551 [windata.nswindow setContentAspectRatio:NSMakeSize(numerator, denominator)];
2552 } else {
2553 [windata.nswindow setContentAspectRatio:NSMakeSize(0, 0)];
2554 }
2555 }
2556}
2557
2558void Cocoa_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h)
2559{
2560 @autoreleasepool {
2561 SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal;
2562 NSView *contentView = windata.sdlContentView;
2563 NSRect viewport = [contentView bounds];
2564
2565 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) {
2566 // This gives us the correct viewport for a Retina-enabled view.
2567 viewport = [contentView convertRectToBacking:viewport];
2568 }
2569
2570 *w = (int)viewport.size.width;
2571 *h = (int)viewport.size.height;
2572 }
2573}
2574
2575void Cocoa_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
2576{
2577 @autoreleasepool {
2578 SDL_CocoaWindowData *windowData = ((__bridge SDL_CocoaWindowData *)window->internal);
2579 NSWindow *nswindow = windowData.nswindow;
2580 bool bActivate = SDL_GetHintBoolean(SDL_HINT_WINDOW_ACTIVATE_WHEN_SHOWN, true);
2581
2582 if (![nswindow isMiniaturized]) {
2583 [windowData.listener pauseVisibleObservation];
2584 if (window->parent) {
2585 NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow;
2586 [nsparent addChildWindow:nswindow ordered:NSWindowAbove];
2587
2588 if (window->flags & SDL_WINDOW_MODAL) {
2589 Cocoa_SetWindowModal(_this, window, true);
2590 }
2591 }
2592 if (!SDL_WINDOW_IS_POPUP(window)) {
2593 if (bActivate) {
2594 [nswindow makeKeyAndOrderFront:nil];
2595 } else {
2596 // Order this window below the key window if we're not activating it
2597 if ([NSApp keyWindow]) {
2598 [nswindow orderWindow:NSWindowBelow relativeTo:[[NSApp keyWindow] windowNumber]];
2599 }
2600 }
2601 }
2602 }
2603 [nswindow setIsVisible:YES];
2604 [windowData.listener resumeVisibleObservation];
2605 }
2606}
2607
2608void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
2609{
2610 @autoreleasepool {
2611 NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow;
2612 const BOOL waskey = [nswindow isKeyWindow];
2613
2614 /* orderOut has no effect on miniaturized windows, so close must be used to remove
2615 * the window from the desktop and window list in this case.
2616 *
2617 * SDL holds a strong reference to the window (oneShot/releasedWhenClosed are 'NO'),
2618 * and calling 'close' doesn't send a 'windowShouldClose' message, so it's safe to
2619 * use for this purpose as nothing is implicitly released.
2620 */
2621 if (![nswindow isMiniaturized]) {
2622 [nswindow orderOut:nil];
2623 } else {
2624 [nswindow close];
2625 }
2626
2627 /* If this window is the source of a modal session, end it when
2628 * hidden, or other windows will be prevented from closing.
2629 */
2630 Cocoa_SetWindowModal(_this, window, false);
2631
2632 // Transfer keyboard focus back to the parent when closing a popup menu
2633 if (window->flags & SDL_WINDOW_POPUP_MENU) {
2634 SDL_Window *new_focus = window->parent;
2635 bool set_focus = window == SDL_GetKeyboardFocus();
2636
2637 // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed.
2638 while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
2639 new_focus = new_focus->parent;
2640
2641 // If some window in the chain currently had focus, set it to the new lowest-level window.
2642 if (!set_focus) {
2643 set_focus = new_focus == SDL_GetKeyboardFocus();
2644 }
2645 }
2646
2647 Cocoa_SetKeyboardFocus(new_focus, set_focus);
2648 } else if (window->parent && waskey) {
2649 /* Key status is not automatically set on the parent when a child is hidden. Check if the
2650 * child window was key, and set the first visible parent to be key if so.
2651 */
2652 SDL_Window *new_focus = window->parent;
2653
2654 while (new_focus->parent != NULL && (new_focus->is_hiding || new_focus->is_destroying)) {
2655 new_focus = new_focus->parent;
2656 }
2657
2658 if (new_focus) {
2659 NSWindow *newkey = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow;
2660 [newkey makeKeyAndOrderFront:nil];
2661 }
2662 }
2663 }
2664}
2665
2666void Cocoa_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window)
2667{
2668 @autoreleasepool {
2669 SDL_CocoaWindowData *windowData = ((__bridge SDL_CocoaWindowData *)window->internal);
2670 NSWindow *nswindow = windowData.nswindow;
2671 bool bActivate = SDL_GetHintBoolean(SDL_HINT_WINDOW_ACTIVATE_WHEN_RAISED, true);
2672
2673 /* makeKeyAndOrderFront: has the side-effect of deminiaturizing and showing
2674 a minimized or hidden window, so check for that before showing it.
2675 */
2676 [windowData.listener pauseVisibleObservation];
2677 if (![nswindow isMiniaturized] && [nswindow isVisible]) {
2678 if (window->parent) {
2679 NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow;
2680 [nsparent addChildWindow:nswindow ordered:NSWindowAbove];
2681 }
2682 if (!SDL_WINDOW_IS_POPUP(window)) {
2683 if (bActivate) {
2684 [NSApp activateIgnoringOtherApps:YES];
2685 [nswindow makeKeyAndOrderFront:nil];
2686 } else {
2687 [nswindow orderFront:nil];
2688 }
2689 } else {
2690 if (bActivate) {
2691 [nswindow makeKeyWindow];
2692 }
2693 }
2694 }
2695 [windowData.listener resumeVisibleObservation];
2696 }
2697}
2698
2699void Cocoa_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window)
2700{
2701 @autoreleasepool {
2702 SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal;
2703 NSWindow *nswindow = windata.nswindow;
2704
2705 if ([windata.listener windowOperationIsPending:(PENDING_OPERATION_ENTER_FULLSCREEN | PENDING_OPERATION_LEAVE_FULLSCREEN)] ||
2706 [windata.listener isInFullscreenSpaceTransition]) {
2707 Cocoa_SyncWindow(_this, window);
2708 }
2709
2710 if (!(window->flags & SDL_WINDOW_FULLSCREEN) &&
2711 ![windata.listener isInFullscreenSpaceTransition] &&
2712 ![windata.listener isInFullscreenSpace]) {
2713 [nswindow zoom:nil];
2714 ScheduleContextUpdates(windata);
2715 } else if (!windata.was_zoomed) {
2716 [windata.listener addPendingWindowOperation:PENDING_OPERATION_ZOOM];
2717 } else {
2718 [windata.listener clearPendingWindowOperation:PENDING_OPERATION_ZOOM];
2719 }
2720 }
2721}
2722
2723void Cocoa_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window)
2724{
2725 @autoreleasepool {
2726 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
2727 NSWindow *nswindow = data.nswindow;
2728
2729 [data.listener addPendingWindowOperation:PENDING_OPERATION_MINIMIZE];
2730 if ([data.listener isInFullscreenSpace] || (window->flags & SDL_WINDOW_FULLSCREEN)) {
2731 [data.listener addPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN];
2732 SDL_UpdateFullscreenMode(window, false, true);
2733 } else if ([data.listener isInFullscreenSpaceTransition]) {
2734 [data.listener addPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN];
2735 } else {
2736 [nswindow miniaturize:nil];
2737 }
2738 }
2739}
2740
2741void Cocoa_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window)
2742{
2743 @autoreleasepool {
2744 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
2745 NSWindow *nswindow = data.nswindow;
2746
2747 if (([data.listener windowOperationIsPending:(PENDING_OPERATION_ENTER_FULLSCREEN | PENDING_OPERATION_LEAVE_FULLSCREEN)] &&
2748 ![data.nswindow isMiniaturized]) || [data.listener isInFullscreenSpaceTransition]) {
2749 Cocoa_SyncWindow(_this, window);
2750 }
2751
2752 [data.listener clearPendingWindowOperation:(PENDING_OPERATION_MINIMIZE)];
2753
2754 if (!(window->flags & SDL_WINDOW_FULLSCREEN) &&
2755 ![data.listener isInFullscreenSpaceTransition] &&
2756 ![data.listener isInFullscreenSpace]) {
2757 if ([nswindow isMiniaturized]) {
2758 [nswindow deminiaturize:nil];
2759 } else if (Cocoa_IsWindowZoomed(window)) {
2760 [nswindow zoom:nil];
2761 }
2762 } else if (data.was_zoomed) {
2763 [data.listener addPendingWindowOperation:PENDING_OPERATION_ZOOM];
2764 } else {
2765 [data.listener clearPendingWindowOperation:PENDING_OPERATION_ZOOM];
2766 }
2767 }
2768}
2769
2770void Cocoa_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered)
2771{
2772 @autoreleasepool {
2773 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
2774
2775 // If the window is in or transitioning to/from fullscreen, this will be set on leave.
2776 if (!(window->flags & SDL_WINDOW_FULLSCREEN) && ![data.listener isInFullscreenSpaceTransition]) {
2777 if (SetWindowStyle(window, GetWindowStyle(window))) {
2778 if (bordered) {
2779 Cocoa_SetWindowTitle(_this, window); // this got blanked out.
2780 }
2781 }
2782 } else {
2783 data.border_toggled = true;
2784 }
2785 Cocoa_UpdateClipCursor(window);
2786 }
2787}
2788
2789void Cocoa_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable)
2790{
2791 @autoreleasepool {
2792 /* Don't set this if we're in or transitioning to/from a space!
2793 * The window will get permanently stuck if resizable is false.
2794 * -flibit
2795 */
2796 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
2797 SDL3Cocoa_WindowListener *listener = data.listener;
2798 NSWindow *nswindow = data.nswindow;
2799 SDL_CocoaVideoData *videodata = data.videodata;
2800 if (![listener isInFullscreenSpace] && ![listener isInFullscreenSpaceTransition]) {
2801 SetWindowStyle(window, GetWindowStyle(window));
2802 }
2803 if (videodata.allow_spaces) {
2804 if (resizable) {
2805 // resizable windows are Spaces-friendly: they get the "go fullscreen" toggle button on their titlebar.
2806 [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
2807 } else {
2808 [nswindow setCollectionBehavior:NSWindowCollectionBehaviorManaged];
2809 }
2810 }
2811 }
2812}
2813
2814void Cocoa_SetWindowAlwaysOnTop(SDL_VideoDevice *_this, SDL_Window *window, bool on_top)
2815{
2816 @autoreleasepool {
2817 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
2818 NSWindow *nswindow = data.nswindow;
2819
2820 // If the window is in or transitioning to/from fullscreen, this will be set on leave.
2821 if (!(window->flags & SDL_WINDOW_FULLSCREEN) && ![data.listener isInFullscreenSpaceTransition]) {
2822 if (on_top) {
2823 [nswindow setLevel:NSFloatingWindowLevel];
2824 } else {
2825 [nswindow setLevel:kCGNormalWindowLevel];
2826 }
2827 }
2828 }
2829}
2830
2831SDL_FullscreenResult Cocoa_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen)
2832{
2833 @autoreleasepool {
2834 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
2835 NSWindow *nswindow = data.nswindow;
2836 NSRect rect;
2837
2838 // This is a synchronous operation, so always clear the pending flags.
2839 [data.listener clearPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN | PENDING_OPERATION_LEAVE_FULLSCREEN];
2840
2841 // The view responder chain gets messed with during setStyleMask
2842 if ([data.sdlContentView nextResponder] == data.listener) {
2843 [data.sdlContentView setNextResponder:nil];
2844 }
2845
2846 if (fullscreen) {
2847 SDL_Rect bounds;
2848
2849 if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
2850 data.was_zoomed = !!(window->flags & SDL_WINDOW_MAXIMIZED);
2851 }
2852
2853 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_ENTER_FULLSCREEN, 0, 0);
2854 Cocoa_GetDisplayBounds(_this, display, &bounds);
2855 rect.origin.x = bounds.x;
2856 rect.origin.y = bounds.y;
2857 rect.size.width = bounds.w;
2858 rect.size.height = bounds.h;
2859 ConvertNSRect(&rect);
2860
2861 /* Hack to fix origin on macOS 10.4
2862 This is no longer needed as of macOS 10.15, according to bug 4822.
2863 */
2864 if (SDL_floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_14) {
2865 NSRect screenRect = [[nswindow screen] frame];
2866 if (screenRect.size.height >= 1.0f) {
2867 rect.origin.y += (screenRect.size.height - rect.size.height);
2868 }
2869 }
2870
2871 [nswindow setStyleMask:NSWindowStyleMaskBorderless];
2872 } else {
2873 NSRect frameRect;
2874
2875 SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0);
2876
2877 rect.origin.x = data.was_zoomed ? window->windowed.x : window->floating.x;
2878 rect.origin.y = data.was_zoomed ? window->windowed.y : window->floating.y;
2879 rect.size.width = data.was_zoomed ? window->windowed.w : window->floating.w;
2880 rect.size.height = data.was_zoomed ? window->windowed.h : window->floating.h;
2881
2882 ConvertNSRect(&rect);
2883
2884 /* The window is not meant to be fullscreen, but its flags might have a
2885 * fullscreen bit set if it's scheduled to go fullscreen immediately
2886 * after. Always using the windowed mode style here works around bugs in
2887 * macOS 10.15 where the window doesn't properly restore the windowed
2888 * mode decorations after exiting fullscreen-desktop, when the window
2889 * was created as fullscreen-desktop. */
2890 [nswindow setStyleMask:GetWindowWindowedStyle(window)];
2891
2892 // Hack to restore window decorations on macOS 10.10
2893 frameRect = [nswindow frame];
2894 [nswindow setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:NO];
2895 [nswindow setFrame:frameRect display:NO];
2896 }
2897
2898 // The view responder chain gets messed with during setStyleMask
2899 if ([data.sdlContentView nextResponder] != data.listener) {
2900 [data.sdlContentView setNextResponder:data.listener];
2901 }
2902
2903 [nswindow setContentSize:rect.size];
2904 [nswindow setFrameOrigin:rect.origin];
2905
2906 // When the window style changes the title is cleared
2907 if (!fullscreen) {
2908 Cocoa_SetWindowTitle(_this, window);
2909 data.was_zoomed = NO;
2910 if ([data.listener windowOperationIsPending:PENDING_OPERATION_ZOOM]) {
2911 [data.listener clearPendingWindowOperation:PENDING_OPERATION_ZOOM];
2912 [nswindow zoom:nil];
2913 }
2914 }
2915
2916 if (SDL_ShouldAllowTopmost() && fullscreen) {
2917 // OpenGL is rendering to the window, so make it visible!
2918 [nswindow setLevel:kCGMainMenuWindowLevel + 1];
2919 } else if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) {
2920 [nswindow setLevel:NSFloatingWindowLevel];
2921 } else {
2922 [nswindow setLevel:kCGNormalWindowLevel];
2923 }
2924
2925 if ([nswindow isVisible] || fullscreen) {
2926 [data.listener pauseVisibleObservation];
2927 [nswindow makeKeyAndOrderFront:nil];
2928 [data.listener resumeVisibleObservation];
2929 }
2930
2931 // Update the safe area insets
2932 // The view never seems to reflect the safe area, so we'll use the screen instead
2933 if (@available(macOS 12.0, *)) {
2934 if (fullscreen) {
2935 NSScreen *screen = [nswindow screen];
2936
2937 SDL_SetWindowSafeAreaInsets(data.window,
2938 (int)SDL_ceilf(screen.safeAreaInsets.left),
2939 (int)SDL_ceilf(screen.safeAreaInsets.right),
2940 (int)SDL_ceilf(screen.safeAreaInsets.top),
2941 (int)SDL_ceilf(screen.safeAreaInsets.bottom));
2942 } else {
2943 SDL_SetWindowSafeAreaInsets(data.window, 0, 0, 0, 0);
2944 }
2945 }
2946
2947 /* When coming out of fullscreen to minimize, this needs to happen after the window
2948 * is made key again, or it won't minimize on 15.0 (Sequoia).
2949 */
2950 if (!fullscreen && [data.listener windowOperationIsPending:PENDING_OPERATION_MINIMIZE]) {
2951 Cocoa_WaitForMiniaturizable(window);
2952 [data.listener addPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN];
2953 [data.listener clearPendingWindowOperation:PENDING_OPERATION_MINIMIZE];
2954 [nswindow miniaturize:nil];
2955 }
2956
2957 ScheduleContextUpdates(data);
2958 Cocoa_SyncWindow(_this, window);
2959 Cocoa_UpdateClipCursor(window);
2960 }
2961
2962 return SDL_FULLSCREEN_SUCCEEDED;
2963}
2964
2965void *Cocoa_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size)
2966{
2967 @autoreleasepool {
2968 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
2969 NSWindow *nswindow = data.nswindow;
2970 NSScreen *screen = [nswindow screen];
2971 NSData *iccProfileData = nil;
2972 void *retIccProfileData = NULL;
2973
2974 if (screen == nil) {
2975 SDL_SetError("Could not get screen of window.");
2976 return NULL;
2977 }
2978
2979 if ([screen colorSpace] == nil) {
2980 SDL_SetError("Could not get colorspace information of screen.");
2981 return NULL;
2982 }
2983
2984 iccProfileData = [[screen colorSpace] ICCProfileData];
2985 if (iccProfileData == nil) {
2986 SDL_SetError("Could not get ICC profile data.");
2987 return NULL;
2988 }
2989
2990 retIccProfileData = SDL_malloc([iccProfileData length]);
2991 if (!retIccProfileData) {
2992 return NULL;
2993 }
2994
2995 [iccProfileData getBytes:retIccProfileData length:[iccProfileData length]];
2996 *size = [iccProfileData length];
2997 return retIccProfileData;
2998 }
2999}
3000
3001SDL_DisplayID Cocoa_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *window)
3002{
3003 @autoreleasepool {
3004 NSScreen *screen;
3005 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3006
3007 // Not recognized via CHECK_WINDOW_MAGIC
3008 if (data == nil) {
3009 // Don't set the error here, it hides other errors and is ignored anyway
3010 // return SDL_SetError("Window data not set");
3011 return 0;
3012 }
3013
3014 // NSWindow.screen may be nil when the window is off-screen.
3015 screen = data.nswindow.screen;
3016
3017 if (screen != nil) {
3018 // https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc
3019 CGDirectDisplayID displayid = [[screen.deviceDescription objectForKey:@"NSScreenNumber"] unsignedIntValue];
3020 SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid);
3021 if (display) {
3022 return display->id;
3023 }
3024 }
3025
3026 // The higher level code will use other logic to find the display
3027 return 0;
3028 }
3029}
3030
3031bool Cocoa_SetWindowMouseRect(SDL_VideoDevice *_this, SDL_Window *window)
3032{
3033 Cocoa_UpdateClipCursor(window);
3034 return true;
3035}
3036
3037bool Cocoa_SetWindowMouseGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed)
3038{
3039 @autoreleasepool {
3040 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3041
3042 Cocoa_UpdateClipCursor(window);
3043
3044 if (data && (window->flags & SDL_WINDOW_FULLSCREEN) != 0) {
3045 if (SDL_ShouldAllowTopmost() && (window->flags & SDL_WINDOW_INPUT_FOCUS) && ![data.listener isInFullscreenSpace]) {
3046 // OpenGL is rendering to the window, so make it visible!
3047 // Doing this in 10.11 while in a Space breaks things (bug #3152)
3048 [data.nswindow setLevel:kCGMainMenuWindowLevel + 1];
3049 } else if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) {
3050 [data.nswindow setLevel:NSFloatingWindowLevel];
3051 } else {
3052 [data.nswindow setLevel:kCGNormalWindowLevel];
3053 }
3054 }
3055 }
3056
3057 return true;
3058}
3059
3060void Cocoa_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
3061{
3062 @autoreleasepool {
3063 SDL_CocoaWindowData *data = (SDL_CocoaWindowData *)CFBridgingRelease(window->internal);
3064
3065 if (data) {
3066#ifdef SDL_VIDEO_OPENGL
3067
3068 NSArray *contexts;
3069
3070#endif // SDL_VIDEO_OPENGL
3071 SDL_Window *topmost = GetParentToplevelWindow(window);
3072 SDL_CocoaWindowData *topmost_data = (__bridge SDL_CocoaWindowData *)topmost->internal;
3073
3074 /* Reset the input focus of the root window if this window is still set as keyboard focus.
3075 * SDL_DestroyWindow will have already taken care of reassigning focus if this is the SDL
3076 * keyboard focus, this ensures that an inactive window with this window set as input focus
3077 * does not try to reference it the next time it gains focus.
3078 */
3079 if (topmost_data.keyboard_focus == window) {
3080 SDL_Window *new_focus = window;
3081 while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) {
3082 new_focus = new_focus->parent;
3083 }
3084
3085 topmost_data.keyboard_focus = new_focus;
3086 }
3087
3088 if ([data.listener isInFullscreenSpace]) {
3089 [NSMenu setMenuBarVisible:YES];
3090 }
3091 [data.listener close];
3092 data.listener = nil;
3093
3094 if (!(window->flags & SDL_WINDOW_EXTERNAL)) {
3095 // Release the content view to avoid further updateLayer callbacks
3096 [data.nswindow setContentView:nil];
3097 [data.nswindow close];
3098 }
3099
3100#ifdef SDL_VIDEO_OPENGL
3101
3102 contexts = [data.nscontexts copy];
3103 for (SDL3OpenGLContext *context in contexts) {
3104 // Calling setWindow:NULL causes the context to remove itself from the context list.
3105 [context setWindow:NULL];
3106 }
3107
3108#endif // SDL_VIDEO_OPENGL
3109 }
3110 window->internal = NULL;
3111 }
3112}
3113
3114bool Cocoa_SetWindowFullscreenSpace(SDL_Window *window, bool state, bool blocking)
3115{
3116 @autoreleasepool {
3117 bool succeeded = false;
3118 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3119
3120 if (state) {
3121 data.fullscreen_space_requested = YES;
3122 }
3123 data.in_blocking_transition = blocking;
3124 if ([data.listener setFullscreenSpace:(state ? YES : NO)]) {
3125 if (blocking) {
3126 const int maxattempts = 3;
3127 int attempt = 0;
3128 while (++attempt <= maxattempts) {
3129 /* Wait for the transition to complete, so application changes
3130 take effect properly (e.g. setting the window size, etc.)
3131 */
3132 const int limit = 10000;
3133 int count = 0;
3134 while ([data.listener isInFullscreenSpaceTransition]) {
3135 if (++count == limit) {
3136 // Uh oh, transition isn't completing. Should we assert?
3137 break;
3138 }
3139 SDL_Delay(1);
3140 SDL_PumpEvents();
3141 }
3142 if ([data.listener isInFullscreenSpace] == (state ? YES : NO)) {
3143 break;
3144 }
3145 // Try again, the last attempt was interrupted by user gestures
3146 if (![data.listener setFullscreenSpace:(state ? YES : NO)]) {
3147 break; // ???
3148 }
3149 }
3150 }
3151
3152 // Return TRUE to prevent non-space fullscreen logic from running
3153 succeeded = true;
3154 }
3155
3156 data.in_blocking_transition = NO;
3157 return succeeded;
3158 }
3159}
3160
3161bool Cocoa_SetWindowHitTest(SDL_Window *window, bool enabled)
3162{
3163 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3164
3165 [data.listener updateHitTest];
3166 return true;
3167}
3168
3169void Cocoa_AcceptDragAndDrop(SDL_Window *window, bool accept)
3170{
3171 @autoreleasepool {
3172 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3173 if (accept) {
3174 [data.nswindow registerForDraggedTypes:@[ (NSString *)kUTTypeFileURL,
3175 (NSString *)kUTTypeUTF8PlainText ]];
3176 } else {
3177 [data.nswindow unregisterDraggedTypes];
3178 }
3179 }
3180}
3181
3182bool Cocoa_SetWindowParent(SDL_VideoDevice *_this, SDL_Window *window, SDL_Window *parent)
3183{
3184 @autoreleasepool {
3185 SDL_CocoaWindowData *child_data = (__bridge SDL_CocoaWindowData *)window->internal;
3186
3187 // Remove an existing parent.
3188 if (child_data.nswindow.parentWindow) {
3189 NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow;
3190 [nsparent removeChildWindow:child_data.nswindow];
3191 }
3192
3193 if (parent) {
3194 SDL_CocoaWindowData *parent_data = (__bridge SDL_CocoaWindowData *)parent->internal;
3195 [parent_data.nswindow addChildWindow:child_data.nswindow ordered:NSWindowAbove];
3196 }
3197 }
3198
3199 return true;
3200}
3201
3202bool Cocoa_SetWindowModal(SDL_VideoDevice *_this, SDL_Window *window, bool modal)
3203{
3204 @autoreleasepool {
3205 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3206
3207 if (data.modal_session) {
3208 [NSApp endModalSession:data.modal_session];
3209 data.modal_session = nil;
3210 }
3211
3212 if (modal) {
3213 data.modal_session = [NSApp beginModalSessionForWindow:data.nswindow];
3214 }
3215 }
3216
3217 return true;
3218}
3219
3220bool Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation)
3221{
3222 @autoreleasepool {
3223 // Note that this is app-wide and not window-specific!
3224 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3225
3226 if (data.flash_request) {
3227 [NSApp cancelUserAttentionRequest:data.flash_request];
3228 data.flash_request = 0;
3229 }
3230
3231 switch (operation) {
3232 case SDL_FLASH_CANCEL:
3233 // Canceled above
3234 break;
3235 case SDL_FLASH_BRIEFLY:
3236 data.flash_request = [NSApp requestUserAttention:NSInformationalRequest];
3237 break;
3238 case SDL_FLASH_UNTIL_FOCUSED:
3239 data.flash_request = [NSApp requestUserAttention:NSCriticalRequest];
3240 break;
3241 default:
3242 return SDL_Unsupported();
3243 }
3244 return true;
3245 }
3246}
3247
3248bool Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable)
3249{
3250 return true; // just succeed, the real work is done elsewhere.
3251}
3252
3253bool Cocoa_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity)
3254{
3255 @autoreleasepool {
3256 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3257 [data.nswindow setAlphaValue:opacity];
3258 return true;
3259 }
3260}
3261
3262bool Cocoa_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window)
3263{
3264 bool result = true;
3265
3266 @autoreleasepool {
3267 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
3268
3269 do {
3270 SDL_PumpEvents();
3271 } while ([data.listener hasPendingWindowOperation]);
3272 }
3273
3274 return result;
3275}
3276
3277#endif // SDL_VIDEO_DRIVER_COCOA