summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/video/cocoa
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/SDL-3.2.8/src/video/cocoa')
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.h34
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.m262
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.h33
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.m680
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.h36
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m604
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.h27
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.m145
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.h66
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.m182
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.h45
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.m716
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.h51
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.m591
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.h88
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.m559
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.h48
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.m156
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.h32
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.m178
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.h28
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.m54
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.h71
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.m337
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.h52
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.m304
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.h199
-rw-r--r--contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m3277
28 files changed, 8855 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.h
new file mode 100644
index 0000000..758f45a
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.h
@@ -0,0 +1,34 @@
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#ifndef SDL_cocoaclipboard_h_
24#define SDL_cocoaclipboard_h_
25
26// Forward declaration
27@class SDL_CocoaVideoData;
28
29extern void Cocoa_CheckClipboardUpdate(SDL_CocoaVideoData *data);
30extern bool Cocoa_SetClipboardData(SDL_VideoDevice *_this);
31extern void *Cocoa_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *size);
32extern bool Cocoa_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type);
33
34#endif // SDL_cocoaclipboard_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.m
new file mode 100644
index 0000000..42c2ad6
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.m
@@ -0,0 +1,262 @@
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 "SDL_cocoavideo.h"
26#include "../../events/SDL_events_c.h"
27#include "../../events/SDL_clipboardevents_c.h"
28
29#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
30
31@interface Cocoa_PasteboardDataProvider : NSObject<NSPasteboardItemDataProvider>
32{
33 SDL_ClipboardDataCallback m_callback;
34 void *m_userdata;
35}
36@end
37
38@implementation Cocoa_PasteboardDataProvider
39
40- (nullable instancetype)initWith:(SDL_ClipboardDataCallback)callback
41 userData:(void *)userdata
42{
43 self = [super init];
44 if (!self) {
45 return self;
46 }
47 m_callback = callback;
48 m_userdata = userdata;
49 return self;
50}
51
52- (void)pasteboard:(NSPasteboard *)pasteboard
53 item:(NSPasteboardItem *)item
54provideDataForType:(NSPasteboardType)type
55{
56 @autoreleasepool {
57 size_t size = 0;
58 CFStringRef mimeType;
59 const void *callbackData;
60 NSData *data;
61 mimeType = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)type, kUTTagClassMIMEType);
62 callbackData = m_callback(m_userdata, [(__bridge NSString *)mimeType UTF8String], &size);
63 CFRelease(mimeType);
64 if (callbackData == NULL || size == 0) {
65 return;
66 }
67 data = [NSData dataWithBytes: callbackData length: size];
68 [item setData: data forType: type];
69 }
70}
71
72@end
73
74static char **GetMimeTypes(int *pnformats)
75{
76 char **new_mime_types = NULL;
77
78 *pnformats = 0;
79
80 int nformats = 0;
81 int formatsSz = 0;
82 NSArray<NSPasteboardItem *> *items = [[NSPasteboard generalPasteboard] pasteboardItems];
83 NSUInteger nitems = [items count];
84 if (nitems > 0) {
85 for (NSPasteboardItem *item in items) {
86 NSArray<NSString *> *types = [item types];
87 for (NSString *type in types) {
88 if (@available(macOS 11.0, *)) {
89 UTType *uttype = [UTType typeWithIdentifier:type];
90 NSString *mime_type = [uttype preferredMIMEType];
91 if (mime_type) {
92 NSUInteger len = [mime_type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
93 formatsSz += len;
94 ++nformats;
95 }
96 }
97 NSUInteger len = [type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
98 formatsSz += len;
99 ++nformats;
100 }
101 }
102
103 new_mime_types = SDL_AllocateTemporaryMemory((nformats + 1) * sizeof(char *) + formatsSz);
104 if (new_mime_types) {
105 int i = 0;
106 char *strPtr = (char *)(new_mime_types + nformats + 1);
107 for (NSPasteboardItem *item in items) {
108 NSArray<NSString *> *types = [item types];
109 for (NSString *type in types) {
110 if (@available(macOS 11.0, *)) {
111 UTType *uttype = [UTType typeWithIdentifier:type];
112 NSString *mime_type = [uttype preferredMIMEType];
113 if (mime_type) {
114 NSUInteger len = [mime_type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
115 SDL_memcpy(strPtr, [mime_type UTF8String], len);
116 new_mime_types[i++] = strPtr;
117 strPtr += len;
118 }
119 }
120 NSUInteger len = [type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
121 SDL_memcpy(strPtr, [type UTF8String], len);
122 new_mime_types[i++] = strPtr;
123 strPtr += len;
124 }
125 }
126
127 new_mime_types[nformats] = NULL;
128 *pnformats = nformats;
129 }
130 }
131 return new_mime_types;
132}
133
134
135void Cocoa_CheckClipboardUpdate(SDL_CocoaVideoData *data)
136{
137 @autoreleasepool {
138 NSPasteboard *pasteboard;
139 NSInteger count;
140
141 pasteboard = [NSPasteboard generalPasteboard];
142 count = [pasteboard changeCount];
143 if (count != data.clipboard_count) {
144 if (count) {
145 int nformats = 0;
146 char **new_mime_types = GetMimeTypes(&nformats);
147 if (new_mime_types) {
148 SDL_SendClipboardUpdate(false, new_mime_types, nformats);
149 }
150 }
151 data.clipboard_count = count;
152 }
153 }
154}
155
156bool Cocoa_SetClipboardData(SDL_VideoDevice *_this)
157{
158 @autoreleasepool {
159 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
160 NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
161 NSPasteboardItem *newItem = [NSPasteboardItem new];
162 NSMutableArray *utiTypes = [NSMutableArray new];
163 Cocoa_PasteboardDataProvider *provider = [[Cocoa_PasteboardDataProvider alloc] initWith: _this->clipboard_callback userData: _this->clipboard_userdata];
164 BOOL itemResult = FALSE;
165 BOOL writeResult = FALSE;
166
167 if (_this->clipboard_callback) {
168 for (int i = 0; i < _this->num_clipboard_mime_types; i++) {
169 CFStringRef mimeType = CFStringCreateWithCString(NULL, _this->clipboard_mime_types[i], kCFStringEncodingUTF8);
170 CFStringRef utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
171 CFRelease(mimeType);
172
173 [utiTypes addObject: (__bridge NSString *)utiType];
174 CFRelease(utiType);
175 }
176 itemResult = [newItem setDataProvider: provider forTypes: utiTypes];
177 if (itemResult == FALSE) {
178 return SDL_SetError("Unable to set clipboard item data");
179 }
180
181 [pasteboard clearContents];
182 writeResult = [pasteboard writeObjects: @[newItem]];
183 if (writeResult == FALSE) {
184 return SDL_SetError("Unable to set clipboard data");
185 }
186 } else {
187 [pasteboard clearContents];
188 }
189 data.clipboard_count = [pasteboard changeCount];
190 }
191 return true;
192}
193
194static bool IsMimeType(const char *tag)
195{
196 if (SDL_strchr(tag, '/')) {
197 // MIME types have slashes
198 return true;
199 } else if (SDL_strchr(tag, '.')) {
200 // UTI identifiers have periods
201 return false;
202 } else {
203 // Not sure, but it's not a UTI identifier
204 return true;
205 }
206}
207
208static CFStringRef GetUTIType(const char *tag)
209{
210 CFStringRef utiType;
211 if (IsMimeType(tag)) {
212 CFStringRef mimeType = CFStringCreateWithCString(NULL, tag, kCFStringEncodingUTF8);
213 utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL);
214 CFRelease(mimeType);
215 } else {
216 utiType = CFStringCreateWithCString(NULL, tag, kCFStringEncodingUTF8);
217 }
218 return utiType;
219}
220
221void *Cocoa_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *size)
222{
223 @autoreleasepool {
224 NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
225 void *data = NULL;
226 *size = 0;
227 for (NSPasteboardItem *item in [pasteboard pasteboardItems]) {
228 NSData *itemData;
229 CFStringRef utiType = GetUTIType(mime_type);
230 itemData = [item dataForType: (__bridge NSString *)utiType];
231 CFRelease(utiType);
232 if (itemData != nil) {
233 NSUInteger length = [itemData length];
234 *size = (size_t)length;
235 data = SDL_malloc(*size + sizeof(Uint32));
236 if (data) {
237 [itemData getBytes: data length: length];
238 SDL_memset((Uint8 *)data + length, 0, sizeof(Uint32));
239 }
240 break;
241 }
242 }
243 return data;
244 }
245}
246
247bool Cocoa_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type)
248{
249 bool result = false;
250 @autoreleasepool {
251 NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
252 CFStringRef utiType = GetUTIType(mime_type);
253 if ([pasteboard canReadItemWithDataConformingToTypes: @[(__bridge NSString *)utiType]]) {
254 result = true;
255 }
256 CFRelease(utiType);
257 }
258 return result;
259
260}
261
262#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.h
new file mode 100644
index 0000000..4944207
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.h
@@ -0,0 +1,33 @@
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#ifndef SDL_cocoaevents_h_
24#define SDL_cocoaevents_h_
25
26extern void Cocoa_RegisterApp(void);
27extern Uint64 Cocoa_GetEventTimestamp(NSTimeInterval nsTimestamp);
28extern void Cocoa_PumpEvents(SDL_VideoDevice *_this);
29extern int Cocoa_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS);
30extern void Cocoa_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window);
31extern bool Cocoa_SuspendScreenSaver(SDL_VideoDevice *_this);
32
33#endif // SDL_cocoaevents_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.m
new file mode 100644
index 0000000..58cae99
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.m
@@ -0,0 +1,680 @@
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 "SDL_cocoavideo.h"
26#include "../../events/SDL_events_c.h"
27
28static SDL_Window *FindSDLWindowForNSWindow(NSWindow *win)
29{
30 SDL_Window *sdlwindow = NULL;
31 SDL_VideoDevice *device = SDL_GetVideoDevice();
32 if (device && device->windows) {
33 for (sdlwindow = device->windows; sdlwindow; sdlwindow = sdlwindow->next) {
34 NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)sdlwindow->internal).nswindow;
35 if (win == nswindow) {
36 return sdlwindow;
37 }
38 }
39 }
40
41 return sdlwindow;
42}
43
44@interface SDL3Application : NSApplication
45
46- (void)terminate:(id)sender;
47- (void)sendEvent:(NSEvent *)theEvent;
48
49+ (void)registerUserDefaults;
50
51@end
52
53@implementation SDL3Application
54
55// Override terminate to handle Quit and System Shutdown smoothly.
56- (void)terminate:(id)sender
57{
58 SDL_SendQuit();
59}
60
61static bool s_bShouldHandleEventsInSDLApplication = false;
62
63static void Cocoa_DispatchEvent(NSEvent *theEvent)
64{
65 SDL_VideoDevice *_this = SDL_GetVideoDevice();
66
67 switch ([theEvent type]) {
68 case NSEventTypeLeftMouseDown:
69 case NSEventTypeOtherMouseDown:
70 case NSEventTypeRightMouseDown:
71 case NSEventTypeLeftMouseUp:
72 case NSEventTypeOtherMouseUp:
73 case NSEventTypeRightMouseUp:
74 case NSEventTypeLeftMouseDragged:
75 case NSEventTypeRightMouseDragged:
76 case NSEventTypeOtherMouseDragged: // usually middle mouse dragged
77 case NSEventTypeMouseMoved:
78 case NSEventTypeScrollWheel:
79 case NSEventTypeMouseEntered:
80 case NSEventTypeMouseExited:
81 Cocoa_HandleMouseEvent(_this, theEvent);
82 break;
83 case NSEventTypeKeyDown:
84 case NSEventTypeKeyUp:
85 case NSEventTypeFlagsChanged:
86 Cocoa_HandleKeyEvent(_this, theEvent);
87 break;
88 default:
89 break;
90 }
91}
92
93// Dispatch events here so that we can handle events caught by
94// nextEventMatchingMask in SDL, as well as events caught by other
95// processes (such as CEF) that are passed down to NSApp.
96- (void)sendEvent:(NSEvent *)theEvent
97{
98 if (s_bShouldHandleEventsInSDLApplication) {
99 Cocoa_DispatchEvent(theEvent);
100 }
101
102 [super sendEvent:theEvent];
103}
104
105+ (void)registerUserDefaults
106{
107 BOOL momentumScrollSupported = (BOOL)SDL_GetHintBoolean(SDL_HINT_MAC_SCROLL_MOMENTUM, false);
108
109 NSDictionary *appDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:
110 [NSNumber numberWithBool:momentumScrollSupported], @"AppleMomentumScrollSupported",
111 [NSNumber numberWithBool:YES], @"ApplePressAndHoldEnabled",
112 [NSNumber numberWithBool:YES], @"ApplePersistenceIgnoreState",
113 nil];
114 [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults];
115}
116
117@end // SDL3Application
118
119// setAppleMenu disappeared from the headers in 10.4
120@interface NSApplication (NSAppleMenu)
121- (void)setAppleMenu:(NSMenu *)menu;
122@end
123
124@interface SDL3AppDelegate : NSObject <NSApplicationDelegate>
125{
126 @public
127 BOOL seenFirstActivate;
128}
129
130- (id)init;
131- (void)localeDidChange:(NSNotification *)notification;
132- (void)observeValueForKeyPath:(NSString *)keyPath
133 ofObject:(id)object
134 change:(NSDictionary *)change
135 context:(void *)context;
136- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app;
137- (IBAction)menu:(id)sender;
138@end
139
140@implementation SDL3AppDelegate : NSObject
141- (id)init
142{
143 self = [super init];
144 if (self) {
145 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
146 bool registerActivationHandlers = SDL_GetHintBoolean("SDL_MAC_REGISTER_ACTIVATION_HANDLERS", true);
147
148 seenFirstActivate = NO;
149
150 if (registerActivationHandlers) {
151 [center addObserver:self
152 selector:@selector(windowWillClose:)
153 name:NSWindowWillCloseNotification
154 object:nil];
155
156 [center addObserver:self
157 selector:@selector(focusSomeWindow:)
158 name:NSApplicationDidBecomeActiveNotification
159 object:nil];
160
161 [center addObserver:self
162 selector:@selector(screenParametersChanged:)
163 name:NSApplicationDidChangeScreenParametersNotification
164 object:nil];
165 }
166
167 [center addObserver:self
168 selector:@selector(localeDidChange:)
169 name:NSCurrentLocaleDidChangeNotification
170 object:nil];
171
172 [NSApp addObserver:self
173 forKeyPath:@"effectiveAppearance"
174 options:NSKeyValueObservingOptionInitial
175 context:nil];
176 }
177
178 return self;
179}
180
181- (void)dealloc
182{
183 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
184
185 [center removeObserver:self name:NSWindowWillCloseNotification object:nil];
186 [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil];
187 [center removeObserver:self name:NSApplicationDidChangeScreenParametersNotification object:nil];
188 [center removeObserver:self name:NSCurrentLocaleDidChangeNotification object:nil];
189 [NSApp removeObserver:self forKeyPath:@"effectiveAppearance"];
190
191 // Remove our URL event handler only if we set it
192 if ([NSApp delegate] == self) {
193 [[NSAppleEventManager sharedAppleEventManager]
194 removeEventHandlerForEventClass:kInternetEventClass
195 andEventID:kAEGetURL];
196 }
197}
198
199- (void)windowWillClose:(NSNotification *)notification
200{
201 NSWindow *win = (NSWindow *)[notification object];
202
203 if (![win isKeyWindow]) {
204 return;
205 }
206
207 // Don't do anything if this was not an SDL window that was closed
208 if (FindSDLWindowForNSWindow(win) == NULL) {
209 return;
210 }
211
212 /* HACK: Make the next window in the z-order key when the key window is
213 * closed. The custom event loop and/or windowing code we have seems to
214 * prevent the normal behavior: https://bugzilla.libsdl.org/show_bug.cgi?id=1825
215 */
216
217 /* +[NSApp orderedWindows] never includes the 'About' window, but we still
218 * want to try its list first since the behavior in other apps is to only
219 * make the 'About' window key if no other windows are on-screen.
220 */
221 for (NSWindow *window in [NSApp orderedWindows]) {
222 if (window != win && [window canBecomeKeyWindow]) {
223 if (![window isOnActiveSpace]) {
224 continue;
225 }
226 [window makeKeyAndOrderFront:self];
227 return;
228 }
229 }
230
231 /* If a window wasn't found above, iterate through all visible windows in
232 * the active Space in z-order (including the 'About' window, if it's shown)
233 * and make the first one key.
234 */
235 for (NSNumber *num in [NSWindow windowNumbersWithOptions:0]) {
236 NSWindow *window = [NSApp windowWithWindowNumber:[num integerValue]];
237 if (window && window != win && [window canBecomeKeyWindow]) {
238 [window makeKeyAndOrderFront:self];
239 return;
240 }
241 }
242}
243
244- (void)focusSomeWindow:(NSNotification *)aNotification
245{
246 SDL_VideoDevice *device;
247 /* HACK: Ignore the first call. The application gets a
248 * applicationDidBecomeActive: a little bit after the first window is
249 * created, and if we don't ignore it, a window that has been created with
250 * SDL_WINDOW_MINIMIZED will ~immediately be restored.
251 */
252 if (!seenFirstActivate) {
253 seenFirstActivate = YES;
254 return;
255 }
256
257 /* Don't do anything if the application already has a key window
258 * that is not an SDL window.
259 */
260 if ([NSApp keyWindow] && FindSDLWindowForNSWindow([NSApp keyWindow]) == NULL) {
261 return;
262 }
263
264 device = SDL_GetVideoDevice();
265 if (device && device->windows) {
266 SDL_Window *window = device->windows;
267 int i;
268 for (i = 0; i < device->num_displays; ++i) {
269 SDL_Window *fullscreen_window = device->displays[i]->fullscreen_window;
270 if (fullscreen_window) {
271 if (fullscreen_window->flags & SDL_WINDOW_MINIMIZED) {
272 SDL_RestoreWindow(fullscreen_window);
273 }
274 return;
275 }
276 }
277
278 if (window->flags & SDL_WINDOW_MINIMIZED) {
279 SDL_RestoreWindow(window);
280 } else {
281 SDL_RaiseWindow(window);
282 }
283 }
284}
285
286- (void)screenParametersChanged:(NSNotification *)aNotification
287{
288 SDL_VideoDevice *device = SDL_GetVideoDevice();
289 if (device) {
290 Cocoa_UpdateDisplays(device);
291 }
292}
293
294- (void)localeDidChange:(NSNotification *)notification
295{
296 SDL_SendLocaleChangedEvent();
297}
298
299- (void)observeValueForKeyPath:(NSString *)keyPath
300 ofObject:(id)object
301 change:(NSDictionary *)change
302 context:(void *)context
303{
304 SDL_SetSystemTheme(Cocoa_GetSystemTheme());
305}
306
307- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
308{
309 return (BOOL)SDL_SendDropFile(NULL, NULL, [filename UTF8String]) && SDL_SendDropComplete(NULL);
310}
311
312- (void)applicationDidFinishLaunching:(NSNotification *)notification
313{
314 if (!SDL_GetHintBoolean("SDL_MAC_REGISTER_ACTIVATION_HANDLERS", true))
315 return;
316
317 /* The menu bar of SDL apps which don't have the typical .app bundle
318 * structure fails to work the first time a window is created (until it's
319 * de-focused and re-focused), if this call is in Cocoa_RegisterApp instead
320 * of here. https://bugzilla.libsdl.org/show_bug.cgi?id=3051
321 */
322 if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, false)) {
323 // Get more aggressive for Catalina: activate the Dock first so we definitely reset all activation state.
324 for (NSRunningApplication *i in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) {
325 [i activateWithOptions:NSApplicationActivateIgnoringOtherApps];
326 break;
327 }
328 SDL_Delay(300); // !!! FIXME: this isn't right.
329 [NSApp activateIgnoringOtherApps:YES];
330 }
331
332 /* If we call this before NSApp activation, macOS might print a complaint
333 * about ApplePersistenceIgnoreState. */
334 [SDL3Application registerUserDefaults];
335}
336
337- (void)handleURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
338{
339 NSString *path = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
340 SDL_SendDropFile(NULL, NULL, [path UTF8String]);
341 SDL_SendDropComplete(NULL);
342}
343
344- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app
345{
346 // This just tells Cocoa that we didn't do any custom save state magic for the app,
347 // so the system is safe to use NSSecureCoding internally, instead of using unencrypted
348 // save states for backwards compatibility. If we don't return YES here, we'll get a
349 // warning on the console at startup:
350 //
351 // ```
352 // WARNING: Secure coding is not enabled for restorable state! Enable secure coding by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState: and returning YES.
353 // ```
354 //
355 // More-detailed explanation:
356 // https://stackoverflow.com/questions/77283578/sonoma-and-nsapplicationdelegate-applicationsupportssecurerestorablestate/77320845#77320845
357 return YES;
358}
359
360- (IBAction)menu:(id)sender
361{
362 SDL_TrayEntry *entry = [[sender representedObject] pointerValue];
363
364 SDL_ClickTrayEntry(entry);
365}
366
367@end
368
369static SDL3AppDelegate *appDelegate = nil;
370
371static NSString *GetApplicationName(void)
372{
373 NSString *appName = nil;
374
375 const char *metaname = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_APP_METADATA_NAME_STRING, NULL);
376 if (metaname && *metaname) {
377 appName = [NSString stringWithUTF8String:metaname];
378 }
379
380 // Determine the application name
381 if (!appName) {
382 appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
383 if (!appName) {
384 appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
385 }
386 }
387
388 if (![appName length]) {
389 appName = [[NSProcessInfo processInfo] processName];
390 }
391
392 return appName;
393}
394
395static bool LoadMainMenuNibIfAvailable(void)
396{
397 NSDictionary *infoDict;
398 NSString *mainNibFileName;
399 bool success = false;
400
401 infoDict = [[NSBundle mainBundle] infoDictionary];
402 if (infoDict) {
403 mainNibFileName = [infoDict valueForKey:@"NSMainNibFile"];
404
405 if (mainNibFileName) {
406 success = [[NSBundle mainBundle] loadNibNamed:mainNibFileName owner:[NSApplication sharedApplication] topLevelObjects:nil];
407 }
408 }
409
410 return success;
411}
412
413static void CreateApplicationMenus(void)
414{
415 NSString *appName;
416 NSString *title;
417 NSMenu *appleMenu;
418 NSMenu *serviceMenu;
419 NSMenu *windowMenu;
420 NSMenuItem *menuItem;
421 NSMenu *mainMenu;
422
423 if (NSApp == nil) {
424 return;
425 }
426
427 mainMenu = [[NSMenu alloc] init];
428
429 // Create the main menu bar
430 [NSApp setMainMenu:mainMenu];
431
432 // Create the application menu
433 appName = GetApplicationName();
434 appleMenu = [[NSMenu alloc] initWithTitle:@""];
435
436 // Add menu items
437 title = [@"About " stringByAppendingString:appName];
438
439 // !!! FIXME: Menu items can't take parameters, just a basic selector, so this should instead call a selector
440 // !!! FIXME: that itself calls -[NSApplication orderFrontStandardAboutPanelWithOptions:optionsDictionary],
441 // !!! FIXME: filling in that NSDictionary with SDL_GetAppMetadataProperty()
442 [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""];
443
444 [appleMenu addItem:[NSMenuItem separatorItem]];
445
446 [appleMenu addItemWithTitle:@"Preferences…" action:nil keyEquivalent:@","];
447
448 [appleMenu addItem:[NSMenuItem separatorItem]];
449
450 serviceMenu = [[NSMenu alloc] initWithTitle:@""];
451 menuItem = [appleMenu addItemWithTitle:@"Services" action:nil keyEquivalent:@""];
452 [menuItem setSubmenu:serviceMenu];
453
454 [NSApp setServicesMenu:serviceMenu];
455
456 [appleMenu addItem:[NSMenuItem separatorItem]];
457
458 title = [@"Hide " stringByAppendingString:appName];
459 [appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"];
460
461 menuItem = [appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"];
462 [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption | NSEventModifierFlagCommand)];
463
464 [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""];
465
466 [appleMenu addItem:[NSMenuItem separatorItem]];
467
468 title = [@"Quit " stringByAppendingString:appName];
469 [appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
470
471 // Put menu into the menubar
472 menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
473 [menuItem setSubmenu:appleMenu];
474 [[NSApp mainMenu] addItem:menuItem];
475
476 // Tell the application object that this is now the application menu
477 [NSApp setAppleMenu:appleMenu];
478
479 // Create the window menu
480 windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
481
482 // Add menu items
483 [windowMenu addItemWithTitle:@"Close" action:@selector(performClose:) keyEquivalent:@"w"];
484
485 [windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"];
486
487 [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
488
489 // Add the fullscreen toggle menu option.
490 /* Cocoa should update the title to Enter or Exit Full Screen automatically.
491 * But if not, then just fallback to Toggle Full Screen.
492 */
493 menuItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"];
494 [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
495 [windowMenu addItem:menuItem];
496
497 // Put menu into the menubar
498 menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""];
499 [menuItem setSubmenu:windowMenu];
500 [[NSApp mainMenu] addItem:menuItem];
501
502 // Tell the application object that this is now the window menu
503 [NSApp setWindowsMenu:windowMenu];
504}
505
506void Cocoa_RegisterApp(void)
507{
508 @autoreleasepool {
509 // This can get called more than once! Be careful what you initialize!
510
511 if (NSApp == nil) {
512 [SDL3Application sharedApplication];
513 SDL_assert(NSApp != nil);
514
515 s_bShouldHandleEventsInSDLApplication = true;
516
517 if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, false)) {
518 [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
519 }
520
521 /* If there aren't already menus in place, look to see if there's
522 * a nib we should use. If not, then manually create the basic
523 * menus we meed.
524 */
525 if ([NSApp mainMenu] == nil) {
526 bool nibLoaded;
527
528 nibLoaded = LoadMainMenuNibIfAvailable();
529 if (!nibLoaded) {
530 CreateApplicationMenus();
531 }
532 }
533 [NSApp finishLaunching];
534 if ([NSApp delegate]) {
535 /* The SDL app delegate calls this in didFinishLaunching if it's
536 * attached to the NSApp, otherwise we need to call it manually.
537 */
538 [SDL3Application registerUserDefaults];
539 }
540 }
541 if (NSApp && !appDelegate) {
542 appDelegate = [[SDL3AppDelegate alloc] init];
543
544 /* If someone else has an app delegate, it means we can't turn a
545 * termination into SDL_Quit, and we can't handle application:openFile:
546 */
547 if (![NSApp delegate]) {
548 /* Only register the URL event handler if we are being set as the
549 * app delegate to avoid replacing any existing event handler.
550 */
551 [[NSAppleEventManager sharedAppleEventManager]
552 setEventHandler:appDelegate
553 andSelector:@selector(handleURLEvent:withReplyEvent:)
554 forEventClass:kInternetEventClass
555 andEventID:kAEGetURL];
556
557 [(NSApplication *)NSApp setDelegate:appDelegate];
558 } else {
559 appDelegate->seenFirstActivate = YES;
560 }
561 }
562 }
563}
564
565Uint64 Cocoa_GetEventTimestamp(NSTimeInterval nsTimestamp)
566{
567 static Uint64 timestamp_offset;
568 Uint64 timestamp = (Uint64)(nsTimestamp * SDL_NS_PER_SECOND);
569 Uint64 now = SDL_GetTicksNS();
570
571 if (!timestamp_offset) {
572 timestamp_offset = (now - timestamp);
573 }
574 timestamp += timestamp_offset;
575
576 if (timestamp > now) {
577 timestamp_offset -= (timestamp - now);
578 timestamp = now;
579 }
580 return timestamp;
581}
582
583int Cocoa_PumpEventsUntilDate(SDL_VideoDevice *_this, NSDate *expiration, bool accumulate)
584{
585 // Run any existing modal sessions.
586 for (SDL_Window *w = _this->windows; w; w = w->next) {
587 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)w->internal;
588 if (data.modal_session) {
589 [NSApp runModalSession:data.modal_session];
590 }
591 }
592
593 for (;;) {
594 NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:YES];
595 if (event == nil) {
596 return 0;
597 }
598
599 if (!s_bShouldHandleEventsInSDLApplication) {
600 Cocoa_DispatchEvent(event);
601 }
602
603 // Pass events down to SDL3Application to be handled in sendEvent:
604 [NSApp sendEvent:event];
605 if (!accumulate) {
606 break;
607 }
608 }
609 return 1;
610}
611
612int Cocoa_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS)
613{
614 @autoreleasepool {
615 if (timeoutNS > 0) {
616 NSDate *limitDate = [NSDate dateWithTimeIntervalSinceNow:(double)timeoutNS / SDL_NS_PER_SECOND];
617 return Cocoa_PumpEventsUntilDate(_this, limitDate, false);
618 } else if (timeoutNS == 0) {
619 return Cocoa_PumpEventsUntilDate(_this, [NSDate distantPast], false);
620 } else {
621 while (Cocoa_PumpEventsUntilDate(_this, [NSDate distantFuture], false) == 0) {
622 }
623 }
624 return 1;
625 }
626}
627
628void Cocoa_PumpEvents(SDL_VideoDevice *_this)
629{
630 @autoreleasepool {
631 Cocoa_PumpEventsUntilDate(_this, [NSDate distantPast], true);
632 }
633}
634
635void Cocoa_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window)
636{
637 @autoreleasepool {
638 NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
639 location:NSMakePoint(0, 0)
640 modifierFlags:0
641 timestamp:0.0
642 windowNumber:((__bridge SDL_CocoaWindowData *)window->internal).window_number
643 context:nil
644 subtype:0
645 data1:0
646 data2:0];
647
648 [NSApp postEvent:event atStart:YES];
649 }
650}
651
652bool Cocoa_SuspendScreenSaver(SDL_VideoDevice *_this)
653{
654 @autoreleasepool {
655 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
656
657 if (data.screensaver_assertion) {
658 IOPMAssertionRelease(data.screensaver_assertion);
659 data.screensaver_assertion = kIOPMNullAssertionID;
660 }
661
662 if (_this->suspend_screensaver) {
663 /* FIXME: this should ideally describe the real reason why the game
664 * called SDL_DisableScreenSaver. Note that the name is only meant to be
665 * seen by macOS power users. there's an additional optional human-readable
666 * (localized) reason parameter which we don't set.
667 */
668 IOPMAssertionID assertion = kIOPMNullAssertionID;
669 NSString *name = [GetApplicationName() stringByAppendingString:@" using SDL_DisableScreenSaver"];
670 IOPMAssertionCreateWithDescription(kIOPMAssertPreventUserIdleDisplaySleep,
671 (__bridge CFStringRef)name,
672 NULL, NULL, NULL, 0, NULL,
673 &assertion);
674 data.screensaver_assertion = assertion;
675 }
676 }
677 return true;
678}
679
680#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.h
new file mode 100644
index 0000000..145f6cf
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.h
@@ -0,0 +1,36 @@
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#ifndef SDL_cocoakeyboard_h_
24#define SDL_cocoakeyboard_h_
25
26extern void Cocoa_InitKeyboard(SDL_VideoDevice *_this);
27extern void Cocoa_HandleKeyEvent(SDL_VideoDevice *_this, NSEvent *event);
28extern void Cocoa_QuitKeyboard(SDL_VideoDevice *_this);
29
30extern bool Cocoa_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props);
31extern bool Cocoa_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window);
32extern bool Cocoa_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window);
33
34extern bool Cocoa_SetWindowKeyboardGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed);
35
36#endif // SDL_cocoakeyboard_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m
new file mode 100644
index 0000000..e458be9
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m
@@ -0,0 +1,604 @@
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 "SDL_cocoavideo.h"
26
27#include "../../events/SDL_events_c.h"
28#include "../../events/SDL_keyboard_c.h"
29#include "../../events/scancodes_darwin.h"
30
31#include <Carbon/Carbon.h>
32
33#if 0
34#define DEBUG_IME NSLog
35#else
36#define DEBUG_IME(...)
37#endif
38
39@interface SDL3TranslatorResponder : NSView <NSTextInputClient>
40{
41 NSString *_markedText;
42 NSRange _markedRange;
43 NSRange _selectedRange;
44 SDL_Rect _inputRect;
45 int _pendingRawCode;
46 SDL_Scancode _pendingScancode;
47 Uint64 _pendingTimestamp;
48}
49- (void)doCommandBySelector:(SEL)myselector;
50- (void)setInputRect:(const SDL_Rect *)rect;
51- (void)setPendingKey:(int)rawcode scancode:(SDL_Scancode)scancode timestamp:(Uint64)timestamp;
52- (void)sendPendingKey;
53- (void)clearPendingKey;
54@end
55
56@implementation SDL3TranslatorResponder
57
58- (void)setInputRect:(const SDL_Rect *)rect
59{
60 SDL_copyp(&_inputRect, rect);
61}
62
63- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange
64{
65 const char *str;
66
67 DEBUG_IME(@"insertText: %@ replacementRange: (%d, %d)", aString,
68 (int)replacementRange.location, (int)replacementRange.length);
69
70 /* Could be NSString or NSAttributedString, so we have
71 * to test and convert it before return as SDL event */
72 if ([aString isKindOfClass:[NSAttributedString class]]) {
73 str = [[aString string] UTF8String];
74 } else {
75 str = [aString UTF8String];
76 }
77
78 // We're likely sending the composed text, so we reset the IME status.
79 if ([self hasMarkedText]) {
80 [self unmarkText];
81 }
82
83 // Deliver the raw key event that generated this text
84 [self sendPendingKey];
85
86 if ((int)replacementRange.location != -1) {
87 // We're replacing the last character
88 SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, true);
89 SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, false);
90 }
91
92 SDL_SendKeyboardText(str);
93}
94
95- (void)doCommandBySelector:(SEL)myselector
96{
97 /* No need to do anything since we are not using Cocoa
98 selectors to handle special keys, instead we use SDL
99 key events to do the same job.
100 */
101}
102
103- (BOOL)hasMarkedText
104{
105 return _markedText != nil;
106}
107
108- (NSRange)markedRange
109{
110 return _markedRange;
111}
112
113- (NSRange)selectedRange
114{
115 return _selectedRange;
116}
117
118- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
119{
120 if ([aString isKindOfClass:[NSAttributedString class]]) {
121 aString = [aString string];
122 }
123
124 if ([aString length] == 0) {
125 [self unmarkText];
126 return;
127 }
128
129 if (_markedText != aString) {
130 _markedText = aString;
131 }
132
133 _selectedRange = selectedRange;
134 _markedRange = NSMakeRange(0, [aString length]);
135
136 // This key event was consumed by the IME
137 [self clearPendingKey];
138
139 NSUInteger utf32SelectedRangeLocation = [[aString substringToIndex:selectedRange.location] lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4;
140 NSUInteger utf32SelectionRangeEnd = [[aString substringToIndex:(selectedRange.location + selectedRange.length)] lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4;
141 NSUInteger utf32SelectionRangeLength = utf32SelectionRangeEnd - utf32SelectedRangeLocation;
142
143 SDL_SendEditingText([aString UTF8String],
144 (int)utf32SelectedRangeLocation, (int)utf32SelectionRangeLength);
145
146 DEBUG_IME(@"setMarkedText: %@, (%d, %d) replacement range (%d, %d)", _markedText,
147 (int)selectedRange.location, (int)selectedRange.length,
148 (int)replacementRange.location, (int)replacementRange.length);
149}
150
151- (void)unmarkText
152{
153 _markedText = nil;
154
155 // This key event was consumed by the IME
156 [self clearPendingKey];
157
158 SDL_SendEditingText("", 0, 0);
159}
160
161- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
162{
163 NSWindow *window = [self window];
164 NSRect contentRect = [window contentRectForFrameRect:[window frame]];
165 float windowHeight = contentRect.size.height;
166 NSRect rect = NSMakeRect(_inputRect.x, windowHeight - _inputRect.y - _inputRect.h,
167 _inputRect.w, _inputRect.h);
168
169 if (actualRange) {
170 *actualRange = aRange;
171 }
172
173 DEBUG_IME(@"firstRectForCharacterRange: (%d, %d): windowHeight = %g, rect = %@",
174 (int)aRange.location, (int)aRange.length, windowHeight,
175 NSStringFromRect(rect));
176
177 rect = [window convertRectToScreen:rect];
178
179 return rect;
180}
181
182- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
183{
184 DEBUG_IME(@"attributedSubstringFromRange: (%d, %d)", (int)aRange.location, (int)aRange.length);
185 return nil;
186}
187
188- (NSInteger)conversationIdentifier
189{
190 return (NSInteger)self;
191}
192
193/* This method returns the index for character that is
194 * nearest to thePoint. thPoint is in screen coordinate system.
195 */
196- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint
197{
198 DEBUG_IME(@"characterIndexForPoint: (%g, %g)", thePoint.x, thePoint.y);
199 return 0;
200}
201
202/* This method is the key to attribute extension.
203 * We could add new attributes through this method.
204 * NSInputServer examines the return value of this
205 * method & constructs appropriate attributed string.
206 */
207- (NSArray *)validAttributesForMarkedText
208{
209 return [NSArray array];
210}
211
212- (void)setPendingKey:(int)rawcode scancode:(SDL_Scancode)scancode timestamp:(Uint64)timestamp
213{
214 _pendingRawCode = rawcode;
215 _pendingScancode = scancode;
216 _pendingTimestamp = timestamp;
217}
218
219- (void)sendPendingKey
220{
221 if (_pendingRawCode < 0) {
222 return;
223 }
224
225 SDL_SendKeyboardKey(_pendingTimestamp, SDL_DEFAULT_KEYBOARD_ID, _pendingRawCode, _pendingScancode, true);
226 [self clearPendingKey];
227}
228
229- (void)clearPendingKey
230{
231 _pendingRawCode = -1;
232}
233
234@end
235
236static bool IsModifierKeyPressed(unsigned int flags,
237 unsigned int target_mask,
238 unsigned int other_mask,
239 unsigned int either_mask)
240{
241 bool target_pressed = (flags & target_mask) != 0;
242 bool other_pressed = (flags & other_mask) != 0;
243 bool either_pressed = (flags & either_mask) != 0;
244
245 if (either_pressed != (target_pressed || other_pressed))
246 return either_pressed;
247
248 return target_pressed;
249}
250
251static void HandleModifiers(SDL_VideoDevice *_this, SDL_Scancode code, unsigned int modifierFlags)
252{
253 bool pressed = false;
254
255 if (code == SDL_SCANCODE_LSHIFT) {
256 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELSHIFTKEYMASK,
257 NX_DEVICERSHIFTKEYMASK, NX_SHIFTMASK);
258 } else if (code == SDL_SCANCODE_LCTRL) {
259 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELCTLKEYMASK,
260 NX_DEVICERCTLKEYMASK, NX_CONTROLMASK);
261 } else if (code == SDL_SCANCODE_LALT) {
262 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELALTKEYMASK,
263 NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK);
264 } else if (code == SDL_SCANCODE_LGUI) {
265 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELCMDKEYMASK,
266 NX_DEVICERCMDKEYMASK, NX_COMMANDMASK);
267 } else if (code == SDL_SCANCODE_RSHIFT) {
268 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERSHIFTKEYMASK,
269 NX_DEVICELSHIFTKEYMASK, NX_SHIFTMASK);
270 } else if (code == SDL_SCANCODE_RCTRL) {
271 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERCTLKEYMASK,
272 NX_DEVICELCTLKEYMASK, NX_CONTROLMASK);
273 } else if (code == SDL_SCANCODE_RALT) {
274 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERALTKEYMASK,
275 NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK);
276 } else if (code == SDL_SCANCODE_RGUI) {
277 pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERCMDKEYMASK,
278 NX_DEVICELCMDKEYMASK, NX_COMMANDMASK);
279 } else {
280 return;
281 }
282
283 if (pressed) {
284 SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, code, true);
285 } else {
286 SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, code, false);
287 }
288}
289
290static void UpdateKeymap(SDL_CocoaVideoData *data, bool send_event)
291{
292 TISInputSourceRef key_layout;
293 UCKeyboardLayout *keyLayoutPtr = NULL;
294 CFDataRef uchrDataRef;
295
296 // See if the keymap needs to be updated
297 key_layout = TISCopyCurrentKeyboardLayoutInputSource();
298 if (key_layout == data.key_layout) {
299 return;
300 }
301 data.key_layout = key_layout;
302
303 // Try Unicode data first
304 uchrDataRef = TISGetInputSourceProperty(key_layout, kTISPropertyUnicodeKeyLayoutData);
305 if (uchrDataRef) {
306 keyLayoutPtr = (UCKeyboardLayout *)CFDataGetBytePtr(uchrDataRef);
307 }
308
309 if (!keyLayoutPtr) {
310 CFRelease(key_layout);
311 return;
312 }
313
314 static struct {
315 int flags;
316 SDL_Keymod modstate;
317 } mods[] = {
318 { 0, SDL_KMOD_NONE },
319 { shiftKey, SDL_KMOD_SHIFT },
320 { alphaLock, SDL_KMOD_CAPS },
321 { (shiftKey | alphaLock), (SDL_KMOD_SHIFT | SDL_KMOD_CAPS) },
322 { optionKey, SDL_KMOD_ALT },
323 { (optionKey | shiftKey), (SDL_KMOD_ALT | SDL_KMOD_SHIFT) },
324 { (optionKey | alphaLock), (SDL_KMOD_ALT | SDL_KMOD_CAPS) },
325 { (optionKey | shiftKey | alphaLock), (SDL_KMOD_ALT | SDL_KMOD_SHIFT | SDL_KMOD_CAPS) }
326 };
327
328 UInt32 keyboard_type = LMGetKbdType();
329
330 SDL_Keymap *keymap = SDL_CreateKeymap();
331 for (int m = 0; m < SDL_arraysize(mods); ++m) {
332 for (int i = 0; i < SDL_arraysize(darwin_scancode_table); i++) {
333 OSStatus err;
334 UniChar s[8];
335 UniCharCount len;
336 UInt32 dead_key_state;
337
338 // Make sure this scancode is a valid character scancode
339 SDL_Scancode scancode = darwin_scancode_table[i];
340 if (scancode == SDL_SCANCODE_UNKNOWN ||
341 scancode == SDL_SCANCODE_DELETE ||
342 (SDL_GetKeymapKeycode(NULL, scancode, SDL_KMOD_NONE) & SDLK_SCANCODE_MASK)) {
343 continue;
344 }
345
346 /*
347 * Swap the scancode for these two wrongly translated keys
348 * UCKeyTranslate() function does not do its job properly for ISO layout keyboards, where the key '@',
349 * which is located in the top left corner of the keyboard right under the Escape key, and the additional
350 * key '<', which is on the right of the Shift key, are inverted
351 */
352 if ((scancode == SDL_SCANCODE_NONUSBACKSLASH || scancode == SDL_SCANCODE_GRAVE) && KBGetLayoutType(LMGetKbdType()) == kKeyboardISO) {
353 // see comments in scancodes_darwin.h
354 scancode = (SDL_Scancode)((SDL_SCANCODE_NONUSBACKSLASH + SDL_SCANCODE_GRAVE) - scancode);
355 }
356
357 dead_key_state = 0;
358 err = UCKeyTranslate(keyLayoutPtr, i, kUCKeyActionDown,
359 ((mods[m].flags >> 8) & 0xFF), keyboard_type,
360 kUCKeyTranslateNoDeadKeysMask,
361 &dead_key_state, 8, &len, s);
362 if (err != noErr) {
363 continue;
364 }
365
366 if (len > 0 && s[0] != 0x10) {
367 SDL_SetKeymapEntry(keymap, scancode, mods[m].modstate, s[0]);
368 } else {
369 // The default keymap doesn't have any SDL_KMOD_ALT entries, so we don't need to override them
370 if (!(mods[m].modstate & SDL_KMOD_ALT)) {
371 SDL_SetKeymapEntry(keymap, scancode, mods[m].modstate, SDLK_UNKNOWN);
372 }
373 }
374 }
375 }
376 SDL_SetKeymap(keymap, send_event);
377}
378
379static void SDLCALL SDL_MacOptionAsAltChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
380{
381 SDL_VideoDevice *_this = (SDL_VideoDevice *)userdata;
382 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
383
384 if (hint && *hint) {
385 if (SDL_strcmp(hint, "none") == 0) {
386 data.option_as_alt = OptionAsAltNone;
387 } else if (SDL_strcmp(hint, "only_left") == 0) {
388 data.option_as_alt = OptionAsAltOnlyLeft;
389 } else if (SDL_strcmp(hint, "only_right") == 0) {
390 data.option_as_alt = OptionAsAltOnlyRight;
391 } else if (SDL_strcmp(hint, "both") == 0) {
392 data.option_as_alt = OptionAsAltBoth;
393 }
394 } else {
395 data.option_as_alt = OptionAsAltNone;
396 }
397}
398
399void Cocoa_InitKeyboard(SDL_VideoDevice *_this)
400{
401 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
402
403 UpdateKeymap(data, false);
404
405 // Set our own names for the platform-dependent but layout-independent keys
406 // This key is NumLock on the MacBook keyboard. :)
407 // SDL_SetScancodeName(SDL_SCANCODE_NUMLOCKCLEAR, "Clear");
408 SDL_SetScancodeName(SDL_SCANCODE_LALT, "Left Option");
409 SDL_SetScancodeName(SDL_SCANCODE_LGUI, "Left Command");
410 SDL_SetScancodeName(SDL_SCANCODE_RALT, "Right Option");
411 SDL_SetScancodeName(SDL_SCANCODE_RGUI, "Right Command");
412
413 data.modifierFlags = (unsigned int)[NSEvent modifierFlags];
414 SDL_ToggleModState(SDL_KMOD_CAPS, (data.modifierFlags & NSEventModifierFlagCapsLock) ? true : false);
415
416 SDL_AddHintCallback(SDL_HINT_MAC_OPTION_AS_ALT, SDL_MacOptionAsAltChanged, _this);
417}
418
419bool Cocoa_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
420{
421 @autoreleasepool {
422 NSView *parentView;
423 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
424 NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow;
425
426 parentView = [nswindow contentView];
427
428 /* We only keep one field editor per process, since only the front most
429 * window can receive text input events, so it make no sense to keep more
430 * than one copy. When we switched to another window and requesting for
431 * text input, simply remove the field editor from its superview then add
432 * it to the front most window's content view */
433 if (!data.fieldEdit) {
434 data.fieldEdit = [[SDL3TranslatorResponder alloc] initWithFrame:NSMakeRect(0.0, 0.0, 0.0, 0.0)];
435 }
436
437 if (![[data.fieldEdit superview] isEqual:parentView]) {
438 // DEBUG_IME(@"add fieldEdit to window contentView");
439 [data.fieldEdit removeFromSuperview];
440 [parentView addSubview:data.fieldEdit];
441 [nswindow makeFirstResponder:data.fieldEdit];
442 }
443 }
444 return Cocoa_UpdateTextInputArea(_this, window);
445}
446
447bool Cocoa_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window)
448{
449 @autoreleasepool {
450 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
451
452 if (data && data.fieldEdit) {
453 [data.fieldEdit removeFromSuperview];
454 data.fieldEdit = nil;
455 }
456 }
457 return true;
458}
459
460bool Cocoa_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window)
461{
462 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
463 if (data.fieldEdit) {
464 [data.fieldEdit setInputRect:&window->text_input_rect];
465 }
466 return true;
467}
468
469static NSEvent *ReplaceEvent(NSEvent *event, OptionAsAlt option_as_alt)
470{
471 if (option_as_alt == OptionAsAltNone) {
472 return event;
473 }
474
475 const unsigned int modflags = (unsigned int)[event modifierFlags];
476
477 bool ignore_alt_characters = false;
478
479 bool lalt_pressed = IsModifierKeyPressed(modflags, NX_DEVICELALTKEYMASK,
480 NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK);
481 bool ralt_pressed = IsModifierKeyPressed(modflags, NX_DEVICERALTKEYMASK,
482 NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK);
483
484 if (option_as_alt == OptionAsAltOnlyLeft && lalt_pressed) {
485 ignore_alt_characters = true;
486 } else if (option_as_alt == OptionAsAltOnlyRight && ralt_pressed) {
487 ignore_alt_characters = true;
488 } else if (option_as_alt == OptionAsAltBoth && (lalt_pressed || ralt_pressed)) {
489 ignore_alt_characters = true;
490 }
491
492 bool cmd_pressed = modflags & NX_COMMANDMASK;
493 bool ctrl_pressed = modflags & NX_CONTROLMASK;
494
495 ignore_alt_characters = ignore_alt_characters && !cmd_pressed && !ctrl_pressed;
496
497 if (ignore_alt_characters) {
498 NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers];
499 return [NSEvent keyEventWithType:[event type]
500 location:[event locationInWindow]
501 modifierFlags:modflags
502 timestamp:[event timestamp]
503 windowNumber:[event windowNumber]
504 context:nil
505 characters:charactersIgnoringModifiers
506 charactersIgnoringModifiers:charactersIgnoringModifiers
507 isARepeat:[event isARepeat]
508 keyCode:[event keyCode]];
509 }
510
511 return event;
512}
513
514void Cocoa_HandleKeyEvent(SDL_VideoDevice *_this, NSEvent *event)
515{
516 unsigned short scancode;
517 SDL_Scancode code;
518 SDL_CocoaVideoData *data = _this ? ((__bridge SDL_CocoaVideoData *)_this->internal) : nil;
519 if (!data) {
520 return; // can happen when returning from fullscreen Space on shutdown
521 }
522
523 if ([event type] == NSEventTypeKeyDown || [event type] == NSEventTypeKeyUp) {
524 event = ReplaceEvent(event, data.option_as_alt);
525 }
526
527 scancode = [event keyCode];
528
529 if ((scancode == 10 || scancode == 50) && KBGetLayoutType(LMGetKbdType()) == kKeyboardISO) {
530 // see comments in scancodes_darwin.h
531 scancode = 60 - scancode;
532 }
533
534 if (scancode < SDL_arraysize(darwin_scancode_table)) {
535 code = darwin_scancode_table[scancode];
536 } else {
537 // Hmm, does this ever happen? If so, need to extend the keymap...
538 code = SDL_SCANCODE_UNKNOWN;
539 }
540
541 switch ([event type]) {
542 case NSEventTypeKeyDown:
543 if (![event isARepeat]) {
544 // See if we need to rebuild the keyboard layout
545 UpdateKeymap(data, true);
546 }
547
548#ifdef DEBUG_SCANCODES
549 if (code == SDL_SCANCODE_UNKNOWN) {
550 SDL_Log("The key you just pressed is not recognized by SDL. To help get this fixed, report this to the SDL forums/mailing list <https://discourse.libsdl.org/> or to Christian Walther <cwalther@gmx.ch>. Mac virtual key code is %d.", scancode);
551 }
552#endif
553 if (SDL_TextInputActive(SDL_GetKeyboardFocus())) {
554 [data.fieldEdit setPendingKey:scancode scancode:code timestamp:Cocoa_GetEventTimestamp([event timestamp])];
555 [data.fieldEdit interpretKeyEvents:[NSArray arrayWithObject:event]];
556 [data.fieldEdit sendPendingKey];
557 } else if (SDL_GetKeyboardFocus()) {
558 SDL_SendKeyboardKey(Cocoa_GetEventTimestamp([event timestamp]), SDL_DEFAULT_KEYBOARD_ID, scancode, code, true);
559 }
560 break;
561 case NSEventTypeKeyUp:
562 SDL_SendKeyboardKey(Cocoa_GetEventTimestamp([event timestamp]), SDL_DEFAULT_KEYBOARD_ID, scancode, code, false);
563 break;
564 case NSEventTypeFlagsChanged: {
565 // see if the new modifierFlags mean any existing keys should be pressed/released...
566 const unsigned int modflags = (unsigned int)[event modifierFlags];
567 HandleModifiers(_this, SDL_SCANCODE_LSHIFT, modflags);
568 HandleModifiers(_this, SDL_SCANCODE_LCTRL, modflags);
569 HandleModifiers(_this, SDL_SCANCODE_LALT, modflags);
570 HandleModifiers(_this, SDL_SCANCODE_LGUI, modflags);
571 HandleModifiers(_this, SDL_SCANCODE_RSHIFT, modflags);
572 HandleModifiers(_this, SDL_SCANCODE_RCTRL, modflags);
573 HandleModifiers(_this, SDL_SCANCODE_RALT, modflags);
574 HandleModifiers(_this, SDL_SCANCODE_RGUI, modflags);
575 break;
576 }
577 default: // just to avoid compiler warnings
578 break;
579 }
580}
581
582void Cocoa_QuitKeyboard(SDL_VideoDevice *_this)
583{
584}
585
586typedef int CGSConnection;
587typedef enum
588{
589 CGSGlobalHotKeyEnable = 0,
590 CGSGlobalHotKeyDisable = 1,
591} CGSGlobalHotKeyOperatingMode;
592
593extern CGSConnection _CGSDefaultConnection(void);
594extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection, CGSGlobalHotKeyOperatingMode mode);
595
596bool Cocoa_SetWindowKeyboardGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed)
597{
598#ifdef SDL_MAC_NO_SANDBOX
599 CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), grabbed ? CGSGlobalHotKeyDisable : CGSGlobalHotKeyEnable);
600#endif
601 return true;
602}
603
604#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.h
new file mode 100644
index 0000000..ea02052
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.h
@@ -0,0 +1,27 @@
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
25extern bool Cocoa_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID);
26
27#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.m
new file mode 100644
index 0000000..d54adb1
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.m
@@ -0,0 +1,145 @@
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 "SDL_cocoavideo.h"
26
27@interface SDL3MessageBoxPresenter : NSObject
28{
29 @public
30 NSInteger clicked;
31 NSWindow *nswindow;
32}
33- (id)initWithParentWindow:(SDL_Window *)window;
34@end
35
36@implementation SDL3MessageBoxPresenter
37- (id)initWithParentWindow:(SDL_Window *)window
38{
39 self = [super init];
40 if (self) {
41 clicked = -1;
42
43 // Retain the NSWindow because we'll show the alert later on the main thread
44 if (window) {
45 nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow;
46 } else {
47 nswindow = nil;
48 }
49 }
50
51 return self;
52}
53
54- (void)showAlert:(NSAlert *)alert
55{
56 if (nswindow) {
57 [alert beginSheetModalForWindow:nswindow
58 completionHandler:^(NSModalResponse returnCode) {
59 [NSApp stopModalWithCode:returnCode];
60 }];
61 clicked = [NSApp runModalForWindow:nswindow];
62 nswindow = nil;
63 } else {
64 clicked = [alert runModal];
65 }
66}
67@end
68
69static void Cocoa_ShowMessageBoxImpl(const SDL_MessageBoxData *messageboxdata, int *buttonID, bool *result)
70{
71 NSAlert *alert;
72 const SDL_MessageBoxButtonData *buttons = messageboxdata->buttons;
73 SDL3MessageBoxPresenter *presenter;
74 NSInteger clicked;
75 int i;
76 Cocoa_RegisterApp();
77
78 alert = [[NSAlert alloc] init];
79
80 if (messageboxdata->flags & SDL_MESSAGEBOX_ERROR) {
81 [alert setAlertStyle:NSAlertStyleCritical];
82 } else if (messageboxdata->flags & SDL_MESSAGEBOX_WARNING) {
83 [alert setAlertStyle:NSAlertStyleWarning];
84 } else {
85 [alert setAlertStyle:NSAlertStyleInformational];
86 }
87
88 [alert setMessageText:[NSString stringWithUTF8String:messageboxdata->title]];
89 [alert setInformativeText:[NSString stringWithUTF8String:messageboxdata->message]];
90
91 for (i = 0; i < messageboxdata->numbuttons; ++i) {
92 const SDL_MessageBoxButtonData *sdlButton;
93 NSButton *button;
94
95 if (messageboxdata->flags & SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT) {
96 sdlButton = &messageboxdata->buttons[messageboxdata->numbuttons - 1 - i];
97 } else {
98 sdlButton = &messageboxdata->buttons[i];
99 }
100
101 button = [alert addButtonWithTitle:[NSString stringWithUTF8String:sdlButton->text]];
102 if (sdlButton->flags & SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT) {
103 [button setKeyEquivalent:@"\r"];
104 } else if (sdlButton->flags & SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT) {
105 [button setKeyEquivalent:@"\033"];
106 } else {
107 [button setKeyEquivalent:@""];
108 }
109 }
110
111 presenter = [[SDL3MessageBoxPresenter alloc] initWithParentWindow:messageboxdata->window];
112
113 [presenter showAlert:alert];
114
115 clicked = presenter->clicked;
116 if (clicked >= NSAlertFirstButtonReturn) {
117 clicked -= NSAlertFirstButtonReturn;
118 if (messageboxdata->flags & SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT) {
119 clicked = messageboxdata->numbuttons - 1 - clicked;
120 }
121 *buttonID = buttons[clicked].buttonID;
122 *result = true;
123 } else {
124 *result = SDL_SetError("Did not get a valid `clicked button' id: %ld", (long)clicked);
125 }
126}
127
128// Display a Cocoa message box
129bool Cocoa_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID)
130{
131 @autoreleasepool {
132 __block bool result = 0;
133
134 if ([NSThread isMainThread]) {
135 Cocoa_ShowMessageBoxImpl(messageboxdata, buttonID, &result);
136 } else {
137 dispatch_sync(dispatch_get_main_queue(), ^{
138 Cocoa_ShowMessageBoxImpl(messageboxdata, buttonID, &result);
139 });
140 }
141 return result;
142 }
143}
144
145#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.h
new file mode 100644
index 0000000..3b76836
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.h
@@ -0,0 +1,66 @@
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/*
22 * @author Mark Callow, www.edgewise-consulting.com.
23 *
24 * Thanks to @slime73 on GitHub for their gist showing how to add a CAMetalLayer
25 * backed view.
26 */
27#include "SDL_internal.h"
28
29#ifndef SDL_cocoametalview_h_
30#define SDL_cocoametalview_h_
31
32#if defined(SDL_VIDEO_DRIVER_COCOA) && (defined(SDL_VIDEO_VULKAN) || defined(SDL_VIDEO_METAL))
33
34#import "../SDL_sysvideo.h"
35
36#import "SDL_cocoawindow.h"
37
38#import <Cocoa/Cocoa.h>
39#import <Metal/Metal.h>
40#import <QuartzCore/CAMetalLayer.h>
41
42@interface SDL3_cocoametalview : NSView
43
44- (instancetype)initWithFrame:(NSRect)frame
45 highDPI:(BOOL)highDPI
46 windowID:(Uint32)windowID
47 opaque:(BOOL)opaque;
48
49- (void)updateDrawableSize;
50- (NSView *)hitTest:(NSPoint)point;
51
52// Override superclass tag so this class can set it.
53@property(assign, readonly) NSInteger tag;
54
55@property(nonatomic) BOOL highDPI;
56@property(nonatomic) Uint32 sdlWindowID;
57
58@end
59
60SDL_MetalView Cocoa_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window);
61void Cocoa_Metal_DestroyView(SDL_VideoDevice *_this, SDL_MetalView view);
62void *Cocoa_Metal_GetLayer(SDL_VideoDevice *_this, SDL_MetalView view);
63
64#endif // SDL_VIDEO_DRIVER_COCOA && (SDL_VIDEO_VULKAN || SDL_VIDEO_METAL)
65
66#endif // SDL_cocoametalview_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.m
new file mode 100644
index 0000000..af84e93
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.m
@@ -0,0 +1,182 @@
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/*
22 * @author Mark Callow, www.edgewise-consulting.com.
23 *
24 * Thanks to @slime73 on GitHub for their gist showing how to add a CAMetalLayer
25 * backed view.
26 */
27#include "SDL_internal.h"
28
29#include "../../events/SDL_windowevents_c.h"
30
31#import "SDL_cocoametalview.h"
32
33#if defined(SDL_VIDEO_DRIVER_COCOA) && (defined(SDL_VIDEO_VULKAN) || defined(SDL_VIDEO_METAL))
34
35static bool SDLCALL SDL_MetalViewEventWatch(void *userdata, SDL_Event *event)
36{
37 /* Update the drawable size when SDL receives a size changed event for
38 * the window that contains the metal view. It would be nice to use
39 * - (void)resizeWithOldSuperviewSize:(NSSize)oldSize and
40 * - (void)viewDidChangeBackingProperties instead, but SDL's size change
41 * events don't always happen in the same frame (for example when a
42 * resizable window exits a fullscreen Space via the user pressing the OS
43 * exit-space button). */
44 if (event->type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
45 @autoreleasepool {
46 SDL3_cocoametalview *view = (__bridge SDL3_cocoametalview *)userdata;
47 if (view.sdlWindowID == event->window.windowID) {
48 [view updateDrawableSize];
49 }
50 }
51 }
52 return false;
53}
54
55@implementation SDL3_cocoametalview
56
57// Return a Metal-compatible layer.
58+ (Class)layerClass
59{
60 return NSClassFromString(@"CAMetalLayer");
61}
62
63// Indicate the view wants to draw using a backing layer instead of drawRect.
64- (BOOL)wantsUpdateLayer
65{
66 return YES;
67}
68
69/* When the wantsLayer property is set to YES, this method will be invoked to
70 * return a layer instance.
71 */
72- (CALayer *)makeBackingLayer
73{
74 return [self.class.layerClass layer];
75}
76
77- (instancetype)initWithFrame:(NSRect)frame
78 highDPI:(BOOL)highDPI
79 windowID:(Uint32)windowID
80 opaque:(BOOL)opaque
81{
82 self = [super initWithFrame:frame];
83 if (self != nil) {
84 self.highDPI = highDPI;
85 self.sdlWindowID = windowID;
86 self.wantsLayer = YES;
87
88 // Allow resize.
89 self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
90
91 self.layer.opaque = opaque;
92
93 SDL_AddWindowEventWatch(SDL_WINDOW_EVENT_WATCH_EARLY, SDL_MetalViewEventWatch, (__bridge void *)(self));
94
95 [self updateDrawableSize];
96 }
97
98 return self;
99}
100
101- (void)dealloc
102{
103 SDL_RemoveWindowEventWatch(SDL_WINDOW_EVENT_WATCH_EARLY, SDL_MetalViewEventWatch, (__bridge void *)(self));
104}
105
106- (NSInteger)tag
107{
108 return SDL_METALVIEW_TAG;
109}
110
111- (void)updateDrawableSize
112{
113 CAMetalLayer *metalLayer = (CAMetalLayer *)self.layer;
114 NSSize size = self.bounds.size;
115 NSSize backingSize = size;
116
117 if (self.highDPI) {
118 /* Note: NSHighResolutionCapable must be set to true in the app's
119 * Info.plist in order for the backing size to be high res.
120 */
121 backingSize = [self convertSizeToBacking:size];
122 }
123
124 metalLayer.contentsScale = backingSize.height / size.height;
125 metalLayer.drawableSize = NSSizeToCGSize(backingSize);
126}
127
128- (NSView *)hitTest:(NSPoint)point
129{
130 return nil;
131}
132
133@end
134
135SDL_MetalView Cocoa_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window)
136{
137 @autoreleasepool {
138 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
139 NSView *view = data.nswindow.contentView;
140 BOOL highDPI = (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) != 0;
141 BOOL opaque = (window->flags & SDL_WINDOW_TRANSPARENT) == 0;
142 Uint32 windowID = SDL_GetWindowID(window);
143 SDL3_cocoametalview *newview;
144 SDL_MetalView metalview;
145
146 newview = [[SDL3_cocoametalview alloc] initWithFrame:view.frame
147 highDPI:highDPI
148 windowID:windowID
149 opaque:opaque];
150 if (newview == nil) {
151 SDL_OutOfMemory();
152 return NULL;
153 }
154
155 [view addSubview:newview];
156
157 // Make sure the drawable size is up to date after attaching the view.
158 [newview updateDrawableSize];
159
160 metalview = (SDL_MetalView)CFBridgingRetain(newview);
161
162 return metalview;
163 }
164}
165
166void Cocoa_Metal_DestroyView(SDL_VideoDevice *_this, SDL_MetalView view)
167{
168 @autoreleasepool {
169 SDL3_cocoametalview *metalview = CFBridgingRelease(view);
170 [metalview removeFromSuperview];
171 }
172}
173
174void *Cocoa_Metal_GetLayer(SDL_VideoDevice *_this, SDL_MetalView view)
175{
176 @autoreleasepool {
177 SDL3_cocoametalview *cocoaview = (__bridge SDL3_cocoametalview *)view;
178 return (__bridge void *)cocoaview.layer;
179 }
180}
181
182#endif // SDL_VIDEO_DRIVER_COCOA && (SDL_VIDEO_VULKAN || SDL_VIDEO_METAL)
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.h
new file mode 100644
index 0000000..37f3aa5
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.h
@@ -0,0 +1,45 @@
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#ifndef SDL_cocoamodes_h_
24#define SDL_cocoamodes_h_
25
26struct SDL_DisplayData
27{
28 CGDirectDisplayID display;
29};
30
31struct SDL_DisplayModeData
32{
33 CFMutableArrayRef modes;
34};
35
36extern void Cocoa_InitModes(SDL_VideoDevice *_this);
37extern void Cocoa_UpdateDisplays(SDL_VideoDevice *_this);
38extern bool Cocoa_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect);
39extern bool Cocoa_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect);
40extern bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display);
41extern bool Cocoa_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode);
42extern void Cocoa_QuitModes(SDL_VideoDevice *_this);
43extern SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid);
44
45#endif // SDL_cocoamodes_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.m
new file mode 100644
index 0000000..b3c34ba
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.m
@@ -0,0 +1,716 @@
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 "SDL_cocoavideo.h"
26#include "../../events/SDL_events_c.h"
27
28// We need this for IODisplayCreateInfoDictionary and kIODisplayOnlyPreferredName
29#include <IOKit/graphics/IOGraphicsLib.h>
30
31// We need this for CVDisplayLinkGetNominalOutputVideoRefreshPeriod
32#include <CoreVideo/CVBase.h>
33#include <CoreVideo/CVDisplayLink.h>
34
35#if (IOGRAPHICSTYPES_REV < 40)
36#define kDisplayModeNativeFlag 0x02000000
37#endif
38
39static bool CG_SetError(const char *prefix, CGDisplayErr result)
40{
41 const char *error;
42
43 switch (result) {
44 case kCGErrorFailure:
45 error = "kCGErrorFailure";
46 break;
47 case kCGErrorIllegalArgument:
48 error = "kCGErrorIllegalArgument";
49 break;
50 case kCGErrorInvalidConnection:
51 error = "kCGErrorInvalidConnection";
52 break;
53 case kCGErrorInvalidContext:
54 error = "kCGErrorInvalidContext";
55 break;
56 case kCGErrorCannotComplete:
57 error = "kCGErrorCannotComplete";
58 break;
59 case kCGErrorNotImplemented:
60 error = "kCGErrorNotImplemented";
61 break;
62 case kCGErrorRangeCheck:
63 error = "kCGErrorRangeCheck";
64 break;
65 case kCGErrorTypeCheck:
66 error = "kCGErrorTypeCheck";
67 break;
68 case kCGErrorInvalidOperation:
69 error = "kCGErrorInvalidOperation";
70 break;
71 case kCGErrorNoneAvailable:
72 error = "kCGErrorNoneAvailable";
73 break;
74 default:
75 error = "Unknown Error";
76 break;
77 }
78 return SDL_SetError("%s: %s", prefix, error);
79}
80
81static NSScreen *GetNSScreenForDisplayID(CGDirectDisplayID displayID)
82{
83 NSArray *screens = [NSScreen screens];
84
85 // !!! FIXME: maybe track the NSScreen in SDL_DisplayData?
86 for (NSScreen *screen in screens) {
87 const CGDirectDisplayID thisDisplay = (CGDirectDisplayID)[[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
88 if (thisDisplay == displayID) {
89 return screen;
90 }
91 }
92 return nil;
93}
94
95SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid)
96{
97 for (int i = 0; i < _this->num_displays; i++) {
98 const SDL_DisplayData *displaydata = _this->displays[i]->internal;
99 if (displaydata && (displaydata->display == displayid)) {
100 return _this->displays[i];
101 }
102 }
103 return NULL;
104}
105
106static float GetDisplayModeRefreshRate(CGDisplayModeRef vidmode, CVDisplayLinkRef link)
107{
108 float refreshRate = (float)CGDisplayModeGetRefreshRate(vidmode);
109
110 // CGDisplayModeGetRefreshRate can return 0 (eg for built-in displays).
111 if (refreshRate == 0 && link != NULL) {
112 CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
113 if ((time.flags & kCVTimeIsIndefinite) == 0 && time.timeValue != 0) {
114 refreshRate = (float)time.timeScale / time.timeValue;
115 }
116 }
117
118 return refreshRate;
119}
120
121static bool HasValidDisplayModeFlags(CGDisplayModeRef vidmode)
122{
123 uint32_t ioflags = CGDisplayModeGetIOFlags(vidmode);
124
125 // Filter out modes which have flags that we don't want.
126 if (ioflags & (kDisplayModeNeverShowFlag | kDisplayModeNotGraphicsQualityFlag)) {
127 return false;
128 }
129
130 // Filter out modes which don't have flags that we want.
131 if (!(ioflags & kDisplayModeValidFlag) || !(ioflags & kDisplayModeSafeFlag)) {
132 return false;
133 }
134
135 return true;
136}
137
138static Uint32 GetDisplayModePixelFormat(CGDisplayModeRef vidmode)
139{
140 // This API is deprecated in 10.11 with no good replacement (as of 10.15).
141 CFStringRef fmt = CGDisplayModeCopyPixelEncoding(vidmode);
142 Uint32 pixelformat = SDL_PIXELFORMAT_UNKNOWN;
143
144 if (CFStringCompare(fmt, CFSTR(IO32BitDirectPixels),
145 kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
146 pixelformat = SDL_PIXELFORMAT_ARGB8888;
147 } else if (CFStringCompare(fmt, CFSTR(IO16BitDirectPixels),
148 kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
149 pixelformat = SDL_PIXELFORMAT_ARGB1555;
150 } else if (CFStringCompare(fmt, CFSTR(kIO30BitDirectPixels),
151 kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
152 pixelformat = SDL_PIXELFORMAT_ARGB2101010;
153 } else {
154 // ignore 8-bit and such for now.
155 }
156
157 CFRelease(fmt);
158
159 return pixelformat;
160}
161
162static bool GetDisplayMode(CGDisplayModeRef vidmode, bool vidmodeCurrent, CFArrayRef modelist, CVDisplayLinkRef link, SDL_DisplayMode *mode)
163{
164 SDL_DisplayModeData *data;
165 bool usableForGUI = CGDisplayModeIsUsableForDesktopGUI(vidmode);
166 size_t width = CGDisplayModeGetWidth(vidmode);
167 size_t height = CGDisplayModeGetHeight(vidmode);
168 size_t pixelW = width;
169 size_t pixelH = height;
170 uint32_t ioflags = CGDisplayModeGetIOFlags(vidmode);
171 float refreshrate = GetDisplayModeRefreshRate(vidmode, link);
172 Uint32 format = GetDisplayModePixelFormat(vidmode);
173 bool interlaced = (ioflags & kDisplayModeInterlacedFlag) != 0;
174 CFMutableArrayRef modes;
175
176 if (format == SDL_PIXELFORMAT_UNKNOWN) {
177 return false;
178 }
179
180 /* Don't fail the current mode based on flags because this could prevent Cocoa_InitModes from
181 * succeeding if the current mode lacks certain flags (esp kDisplayModeSafeFlag). */
182 if (!vidmodeCurrent && !HasValidDisplayModeFlags(vidmode)) {
183 return false;
184 }
185
186 modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
187 CFArrayAppendValue(modes, vidmode);
188
189 /* If a list of possible display modes is passed in, use it to filter out
190 * modes that have duplicate sizes. We don't just rely on SDL's higher level
191 * duplicate filtering because this code can choose what properties are
192 * preferred, and it can add CGDisplayModes to the DisplayModeData's list of
193 * modes to try (see comment below for why that's necessary). */
194 pixelW = CGDisplayModeGetPixelWidth(vidmode);
195 pixelH = CGDisplayModeGetPixelHeight(vidmode);
196
197 if (modelist != NULL) {
198 CFIndex modescount = CFArrayGetCount(modelist);
199 int i;
200
201 for (i = 0; i < modescount; i++) {
202 size_t otherW, otherH, otherpixelW, otherpixelH;
203 float otherrefresh;
204 Uint32 otherformat;
205 bool otherGUI;
206 CGDisplayModeRef othermode = (CGDisplayModeRef)CFArrayGetValueAtIndex(modelist, i);
207 uint32_t otherioflags = CGDisplayModeGetIOFlags(othermode);
208
209 if (CFEqual(vidmode, othermode)) {
210 continue;
211 }
212
213 if (!HasValidDisplayModeFlags(othermode)) {
214 continue;
215 }
216
217 otherW = CGDisplayModeGetWidth(othermode);
218 otherH = CGDisplayModeGetHeight(othermode);
219 otherpixelW = CGDisplayModeGetPixelWidth(othermode);
220 otherpixelH = CGDisplayModeGetPixelHeight(othermode);
221 otherrefresh = GetDisplayModeRefreshRate(othermode, link);
222 otherformat = GetDisplayModePixelFormat(othermode);
223 otherGUI = CGDisplayModeIsUsableForDesktopGUI(othermode);
224
225 /* Ignore this mode if it's interlaced and there's a non-interlaced
226 * mode in the list with the same properties.
227 */
228 if (interlaced && ((otherioflags & kDisplayModeInterlacedFlag) == 0) && width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && refreshrate == otherrefresh && format == otherformat && usableForGUI == otherGUI) {
229 CFRelease(modes);
230 return false;
231 }
232
233 /* Ignore this mode if it's not usable for desktop UI and its
234 * properties are equal to another GUI-capable mode in the list.
235 */
236 if (width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && !usableForGUI && otherGUI && refreshrate == otherrefresh && format == otherformat) {
237 CFRelease(modes);
238 return false;
239 }
240
241 /* If multiple modes have the exact same properties, they'll all
242 * go in the list of modes to try when SetDisplayMode is called.
243 * This is needed because kCGDisplayShowDuplicateLowResolutionModes
244 * (which is used to expose highdpi display modes) can make the
245 * list of modes contain duplicates (according to their properties
246 * obtained via public APIs) which don't work with SetDisplayMode.
247 * Those duplicate non-functional modes *do* have different pixel
248 * formats according to their internal data structure viewed with
249 * NSLog, but currently no public API can detect that.
250 * https://bugzilla.libsdl.org/show_bug.cgi?id=4822
251 *
252 * As of macOS 10.15.0, those duplicates have the exact same
253 * properties via public APIs in every way (even their IO flags and
254 * CGDisplayModeGetIODisplayModeID is the same), so we could test
255 * those for equality here too, but I'm intentionally not doing that
256 * in case there are duplicate modes with different IO flags or IO
257 * display mode IDs in the future. In that case I think it's better
258 * to try them all in SetDisplayMode than to risk one of them being
259 * correct but it being filtered out by SDL_AddFullscreenDisplayMode
260 * as being a duplicate.
261 */
262 if (width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && usableForGUI == otherGUI && refreshrate == otherrefresh && format == otherformat) {
263 CFArrayAppendValue(modes, othermode);
264 }
265 }
266 }
267
268 SDL_zerop(mode);
269 data = (SDL_DisplayModeData *)SDL_malloc(sizeof(*data));
270 if (!data) {
271 CFRelease(modes);
272 return false;
273 }
274 data->modes = modes;
275 mode->format = format;
276 mode->w = (int)width;
277 mode->h = (int)height;
278 mode->pixel_density = (float)pixelW / width;
279 mode->refresh_rate = refreshrate;
280 mode->internal = data;
281 return true;
282}
283
284static char *Cocoa_GetDisplayName(CGDirectDisplayID displayID)
285{
286 if (@available(macOS 10.15, *)) {
287 NSScreen *screen = GetNSScreenForDisplayID(displayID);
288 if (screen) {
289 const char *name = [screen.localizedName UTF8String];
290 if (name) {
291 return SDL_strdup(name);
292 }
293 }
294 }
295
296 // This API is deprecated in 10.9 with no good replacement (as of 10.15).
297 io_service_t servicePort = CGDisplayIOServicePort(displayID);
298 CFDictionaryRef deviceInfo = IODisplayCreateInfoDictionary(servicePort, kIODisplayOnlyPreferredName);
299 NSDictionary *localizedNames = [(__bridge NSDictionary *)deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]];
300 char *displayName = NULL;
301
302 if ([localizedNames count] > 0) {
303 displayName = SDL_strdup([[localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]] UTF8String]);
304 }
305 CFRelease(deviceInfo);
306 return displayName;
307}
308
309static void Cocoa_GetHDRProperties(CGDirectDisplayID displayID, SDL_HDROutputProperties *HDR)
310{
311 HDR->SDR_white_level = 1.0f;
312 HDR->HDR_headroom = 1.0f;
313
314 if (@available(macOS 10.15, *)) {
315 NSScreen *screen = GetNSScreenForDisplayID(displayID);
316 if (screen) {
317 if (screen.maximumExtendedDynamicRangeColorComponentValue > 1.0f) {
318 HDR->HDR_headroom = screen.maximumExtendedDynamicRangeColorComponentValue;
319 } else {
320 HDR->HDR_headroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue;
321 }
322 }
323 }
324}
325
326
327bool Cocoa_AddDisplay(CGDirectDisplayID display, bool send_event)
328{
329 CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(display);
330 if (!moderef) {
331 return false;
332 }
333
334 SDL_DisplayData *displaydata = (SDL_DisplayData *)SDL_malloc(sizeof(*displaydata));
335 if (!displaydata) {
336 CGDisplayModeRelease(moderef);
337 return false;
338 }
339 displaydata->display = display;
340
341 CVDisplayLinkRef link = NULL;
342 CVDisplayLinkCreateWithCGDisplay(display, &link);
343
344 SDL_VideoDisplay viddisplay;
345 SDL_zero(viddisplay);
346 viddisplay.name = Cocoa_GetDisplayName(display); // this returns a strdup'ed string
347
348 SDL_DisplayMode mode;
349 if (!GetDisplayMode(moderef, true, NULL, link, &mode)) {
350 CVDisplayLinkRelease(link);
351 CGDisplayModeRelease(moderef);
352 SDL_free(viddisplay.name);
353 SDL_free(displaydata);
354 return false;
355 }
356
357 CVDisplayLinkRelease(link);
358 CGDisplayModeRelease(moderef);
359
360 Cocoa_GetHDRProperties(displaydata->display, &viddisplay.HDR);
361
362 viddisplay.desktop_mode = mode;
363 viddisplay.internal = displaydata;
364 const bool retval = SDL_AddVideoDisplay(&viddisplay, send_event);
365 SDL_free(viddisplay.name);
366 return retval;
367}
368
369static void Cocoa_DisplayReconfigurationCallback(CGDirectDisplayID displayid, CGDisplayChangeSummaryFlags flags, void *userInfo)
370{
371 #if 0
372 SDL_Log("COCOA DISPLAY RECONFIG CALLBACK! display=%u", (unsigned int) displayid);
373 #define CHECK_DISPLAY_RECONFIG_FLAG(x) if (flags & x) { SDL_Log(" - " #x); }
374 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayBeginConfigurationFlag);
375 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMovedFlag);
376 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetMainFlag);
377 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetModeFlag);
378 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayAddFlag);
379 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayRemoveFlag);
380 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayEnabledFlag);
381 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDisabledFlag);
382 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMirrorFlag);
383 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayUnMirrorFlag);
384 CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDesktopShapeChangedFlag);
385 #undef CHECK_DISPLAY_RECONFIG_FLAG
386 #endif
387
388 SDL_VideoDevice *_this = (SDL_VideoDevice *) userInfo;
389 SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid); // will be NULL for newly-added (or newly-unmirrored) displays!
390
391 if (flags & kCGDisplayDisabledFlag) {
392 flags |= kCGDisplayRemoveFlag; // treat this like a display leaving, even though it's still plugged in.
393 }
394
395 if (flags & kCGDisplayEnabledFlag) {
396 flags |= kCGDisplayAddFlag; // treat this like a display leaving, even though it's still plugged in.
397 }
398
399 if (flags & kCGDisplayMirrorFlag) {
400 flags |= kCGDisplayRemoveFlag; // treat this like a display leaving, even though it's still actually here.
401 }
402
403 if (flags & kCGDisplayUnMirrorFlag) {
404 flags |= kCGDisplayAddFlag; // treat this like a new display arriving, even though it was here all along.
405 }
406
407 if ((flags & kCGDisplayAddFlag) && (flags & kCGDisplayRemoveFlag)) {
408 // sometimes you get a removed device that gets Add and Remove flags at the same time but the display dimensions are 0x0 or 1x1, hence the `> 1` test.
409 // Mirrored things are always removed, since they don't represent a discrete display in this state.
410 if (((flags & kCGDisplayMirrorFlag) == 0) && (CGDisplayPixelsWide(displayid) > 1)) {
411 // Final state is connected
412 flags &= ~kCGDisplayRemoveFlag;
413 } else {
414 // Final state is disconnected
415 flags &= ~kCGDisplayAddFlag;
416 }
417 }
418
419 if (flags & kCGDisplayAddFlag) {
420 if (!display) {
421 if (!Cocoa_AddDisplay(displayid, true)) {
422 return; // oh well.
423 }
424 display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid);
425 SDL_assert(display != NULL);
426 }
427 }
428
429 if (flags & kCGDisplayRemoveFlag) {
430 if (display) {
431 SDL_DelVideoDisplay(display->id, true);
432 display = NULL;
433 }
434 }
435
436 if (flags & kCGDisplaySetModeFlag) {
437 if (display) {
438 CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(displayid);
439 if (moderef) {
440 CVDisplayLinkRef link = NULL;
441 CVDisplayLinkCreateWithCGDisplay(displayid, &link);
442 if (link) {
443 SDL_DisplayMode mode;
444 if (GetDisplayMode(moderef, true, NULL, link, &mode)) {
445 SDL_SetDesktopDisplayMode(display, &mode);
446 }
447 CVDisplayLinkRelease(link);
448 }
449 CGDisplayModeRelease(moderef);
450 }
451 }
452 }
453
454 if (flags & kCGDisplaySetMainFlag) {
455 if (display) {
456 for (int i = 0; i < _this->num_displays; i++) {
457 if (_this->displays[i] == display) {
458 if (i > 0) {
459 // move this display to the front of _this->displays so it's treated as primary.
460 SDL_memmove(&_this->displays[1], &_this->displays[0], sizeof (*_this->displays) * i);
461 _this->displays[0] = display;
462 }
463 flags |= kCGDisplayMovedFlag; // we don't have an SDL event atm for "this display became primary," so at least let everyone know it "moved".
464 break;
465 }
466 }
467 }
468 }
469
470 if (flags & kCGDisplayMovedFlag) {
471 if (display) {
472 SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_MOVED, 0, 0);
473 }
474 }
475
476 if (flags & kCGDisplayDesktopShapeChangedFlag) {
477 SDL_UpdateDesktopBounds();
478 }
479}
480
481void Cocoa_InitModes(SDL_VideoDevice *_this)
482{
483 @autoreleasepool {
484 CGDisplayErr result;
485 CGDisplayCount numDisplays = 0;
486
487 result = CGGetOnlineDisplayList(0, NULL, &numDisplays);
488 if (result != kCGErrorSuccess) {
489 CG_SetError("CGGetOnlineDisplayList()", result);
490 return;
491 }
492
493 bool isstack;
494 CGDirectDisplayID *displays = SDL_small_alloc(CGDirectDisplayID, numDisplays, &isstack);
495
496 result = CGGetOnlineDisplayList(numDisplays, displays, &numDisplays);
497 if (result != kCGErrorSuccess) {
498 CG_SetError("CGGetOnlineDisplayList()", result);
499 SDL_small_free(displays, isstack);
500 return;
501 }
502
503 // future updates to the display graph will come through this callback.
504 CGDisplayRegisterReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this);
505
506 // Pick up the primary display in the first pass, then get the rest
507 for (int pass = 0; pass < 2; ++pass) {
508 for (int i = 0; i < numDisplays; ++i) {
509 if (pass == 0) {
510 if (!CGDisplayIsMain(displays[i])) {
511 continue;
512 }
513 } else {
514 if (CGDisplayIsMain(displays[i])) {
515 continue;
516 }
517 }
518
519 if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay) {
520 continue;
521 }
522
523 Cocoa_AddDisplay(displays[i], false);
524 }
525 }
526 SDL_small_free(displays, isstack);
527 }
528}
529
530void Cocoa_UpdateDisplays(SDL_VideoDevice *_this)
531{
532 SDL_HDROutputProperties HDR;
533 int i;
534
535 for (i = 0; i < _this->num_displays; ++i) {
536 SDL_VideoDisplay *display = _this->displays[i];
537 SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
538
539 Cocoa_GetHDRProperties(displaydata->display, &HDR);
540 SDL_SetDisplayHDRProperties(display, &HDR);
541 }
542}
543
544bool Cocoa_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect)
545{
546 SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
547 CGRect cgrect;
548
549 cgrect = CGDisplayBounds(displaydata->display);
550 rect->x = (int)cgrect.origin.x;
551 rect->y = (int)cgrect.origin.y;
552 rect->w = (int)cgrect.size.width;
553 rect->h = (int)cgrect.size.height;
554 return true;
555}
556
557bool Cocoa_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect)
558{
559 SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
560 NSScreen *screen = GetNSScreenForDisplayID(displaydata->display);
561
562 if (screen == nil) {
563 return SDL_SetError("Couldn't get NSScreen for display");
564 }
565
566 {
567 const NSRect frame = [screen visibleFrame];
568 rect->x = (int)frame.origin.x;
569 rect->y = (int)(CGDisplayPixelsHigh(kCGDirectMainDisplay) - frame.origin.y - frame.size.height);
570 rect->w = (int)frame.size.width;
571 rect->h = (int)frame.size.height;
572 }
573
574 return true;
575}
576
577bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display)
578{
579 SDL_DisplayData *data = (SDL_DisplayData *)display->internal;
580 CVDisplayLinkRef link = NULL;
581 CFArrayRef modes;
582 CFDictionaryRef dict = NULL;
583 const CFStringRef dictkeys[] = { kCGDisplayShowDuplicateLowResolutionModes };
584 const CFBooleanRef dictvalues[] = { kCFBooleanTrue };
585
586 CVDisplayLinkCreateWithCGDisplay(data->display, &link);
587
588 /* By default, CGDisplayCopyAllDisplayModes will only get a subset of the
589 * system's available modes. For example on a 15" 2016 MBP, users can
590 * choose 1920x1080@2x in System Preferences but it won't show up here,
591 * unless we specify the option below.
592 * The display modes returned by CGDisplayCopyAllDisplayModes are also not
593 * high dpi-capable unless this option is set.
594 * macOS 10.15 also seems to have a bug where entering, exiting, and
595 * re-entering exclusive fullscreen with a low dpi display mode can cause
596 * the content of the screen to move up, which this setting avoids:
597 * https://bugzilla.libsdl.org/show_bug.cgi?id=4822
598 */
599
600 dict = CFDictionaryCreate(NULL,
601 (const void **)dictkeys,
602 (const void **)dictvalues,
603 1,
604 &kCFCopyStringDictionaryKeyCallBacks,
605 &kCFTypeDictionaryValueCallBacks);
606
607 modes = CGDisplayCopyAllDisplayModes(data->display, dict);
608
609 if (dict) {
610 CFRelease(dict);
611 }
612
613 if (modes) {
614 CFIndex i;
615 const CFIndex count = CFArrayGetCount(modes);
616
617 for (i = 0; i < count; i++) {
618 CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i);
619 SDL_DisplayMode mode;
620
621 if (GetDisplayMode(moderef, false, modes, link, &mode)) {
622 if (!SDL_AddFullscreenDisplayMode(display, &mode)) {
623 CFRelease(mode.internal->modes);
624 SDL_free(mode.internal);
625 }
626 }
627 }
628
629 CFRelease(modes);
630 }
631
632 CVDisplayLinkRelease(link);
633 return true;
634}
635
636static CGError SetDisplayModeForDisplay(CGDirectDisplayID display, SDL_DisplayModeData *data)
637{
638 /* SDL_DisplayModeData can contain multiple CGDisplayModes to try (with
639 * identical properties), some of which might not work. See GetDisplayMode.
640 */
641 CGError result = kCGErrorFailure;
642 for (CFIndex i = 0; i < CFArrayGetCount(data->modes); i++) {
643 CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(data->modes, i);
644 result = CGDisplaySetDisplayMode(display, moderef, NULL);
645 if (result == kCGErrorSuccess) {
646 // If this mode works, try it first next time.
647 if (i > 0) {
648 CFArrayExchangeValuesAtIndices(data->modes, i, 0);
649 }
650 break;
651 }
652 }
653 return result;
654}
655
656bool Cocoa_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode)
657{
658 SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
659 SDL_DisplayModeData *data = mode->internal;
660 CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken;
661 CGError result = kCGErrorSuccess;
662
663 b_inModeTransition = true;
664
665 // Fade to black to hide resolution-switching flicker
666 if (CGAcquireDisplayFadeReservation(5, &fade_token) == kCGErrorSuccess) {
667 CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, TRUE);
668 }
669
670 if (data == display->desktop_mode.internal) {
671 // Restoring desktop mode
672 SetDisplayModeForDisplay(displaydata->display, data);
673 } else {
674 // Do the physical switch
675 result = SetDisplayModeForDisplay(displaydata->display, data);
676 }
677
678 // Fade in again (asynchronously)
679 if (fade_token != kCGDisplayFadeReservationInvalidToken) {
680 CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE);
681 CGReleaseDisplayFadeReservation(fade_token);
682 }
683
684 b_inModeTransition = false;
685
686 if (result != kCGErrorSuccess) {
687 return CG_SetError("CGDisplaySwitchToMode()", result);
688 }
689 return true;
690}
691
692void Cocoa_QuitModes(SDL_VideoDevice *_this)
693{
694 int i, j;
695
696 CGDisplayRemoveReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this);
697
698 for (i = 0; i < _this->num_displays; ++i) {
699 SDL_VideoDisplay *display = _this->displays[i];
700 SDL_DisplayModeData *mode;
701
702 if (display->current_mode->internal != display->desktop_mode.internal) {
703 Cocoa_SetDisplayMode(_this, display, &display->desktop_mode);
704 }
705
706 mode = display->desktop_mode.internal;
707 CFRelease(mode->modes);
708
709 for (j = 0; j < display->num_fullscreen_modes; j++) {
710 mode = display->fullscreen_modes[j].internal;
711 CFRelease(mode->modes);
712 }
713 }
714}
715
716#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.h
new file mode 100644
index 0000000..70282be
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.h
@@ -0,0 +1,51 @@
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#ifndef SDL_cocoamouse_h_
24#define SDL_cocoamouse_h_
25
26#include "SDL_cocoavideo.h"
27
28extern bool Cocoa_InitMouse(SDL_VideoDevice *_this);
29extern NSWindow *Cocoa_GetMouseFocus();
30extern void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event);
31extern void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event);
32extern void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y);
33extern void Cocoa_QuitMouse(SDL_VideoDevice *_this);
34
35typedef struct
36{
37 // Whether we've seen a cursor warp since the last move event.
38 bool seenWarp;
39 // What location our last cursor warp was to.
40 CGFloat lastWarpX;
41 CGFloat lastWarpY;
42 // What location we last saw the cursor move to.
43 CGFloat lastMoveX;
44 CGFloat lastMoveY;
45} SDL_MouseData;
46
47@interface NSCursor (InvisibleCursor)
48+ (NSCursor *)invisibleCursor;
49@end
50
51#endif // SDL_cocoamouse_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.m
new file mode 100644
index 0000000..530ca0c
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.m
@@ -0,0 +1,591 @@
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 "SDL_cocoamouse.h"
26#include "SDL_cocoavideo.h"
27
28#include "../../events/SDL_mouse_c.h"
29
30#if 0
31#define DEBUG_COCOAMOUSE
32#endif
33
34#ifdef DEBUG_COCOAMOUSE
35#define DLog(fmt, ...) printf("%s: " fmt "\n", __func__, ##__VA_ARGS__)
36#else
37#define DLog(...) \
38 do { \
39 } while (0)
40#endif
41
42@implementation NSCursor (InvisibleCursor)
43+ (NSCursor *)invisibleCursor
44{
45 static NSCursor *invisibleCursor = NULL;
46 if (!invisibleCursor) {
47 // RAW 16x16 transparent GIF
48 static unsigned char cursorBytes[] = {
49 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80,
50 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04,
51 0x01, 0x00, 0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x10,
52 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x8C, 0x8F, 0xA9, 0xCB, 0xED,
53 0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B
54 };
55
56 NSData *cursorData = [NSData dataWithBytesNoCopy:&cursorBytes[0]
57 length:sizeof(cursorBytes)
58 freeWhenDone:NO];
59 NSImage *cursorImage = [[NSImage alloc] initWithData:cursorData];
60 invisibleCursor = [[NSCursor alloc] initWithImage:cursorImage
61 hotSpot:NSZeroPoint];
62 }
63
64 return invisibleCursor;
65}
66@end
67
68static SDL_Cursor *Cocoa_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
69{
70 @autoreleasepool {
71 NSImage *nsimage;
72 NSCursor *nscursor = NULL;
73 SDL_Cursor *cursor = NULL;
74
75 nsimage = Cocoa_CreateImage(surface);
76 if (nsimage) {
77 nscursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSMakePoint(hot_x, hot_y)];
78 }
79
80 if (nscursor) {
81 cursor = SDL_calloc(1, sizeof(*cursor));
82 if (cursor) {
83 cursor->internal = (void *)CFBridgingRetain(nscursor);
84 }
85 }
86
87 return cursor;
88 }
89}
90
91/* there are .pdf files of some of the cursors we need, installed by default on macOS, but not available through NSCursor.
92 If we can load them ourselves, use them, otherwise fallback to something standard but not super-great.
93 Since these are under /System, they should be available even to sandboxed apps. */
94static NSCursor *LoadHiddenSystemCursor(NSString *cursorName, SEL fallback)
95{
96 NSString *cursorPath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:cursorName];
97 NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"info.plist"]];
98 // we can't do animation atm. :/
99 const int frames = (int)[[info valueForKey:@"frames"] integerValue];
100 NSCursor *cursor;
101 NSImage *image = [[NSImage alloc] initWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"cursor.pdf"]];
102 if ((image == nil) || (image.isValid == NO)) {
103 return [NSCursor performSelector:fallback];
104 }
105
106 if (frames > 1) {
107#ifdef MAC_OS_VERSION_12_0 // same value as deprecated symbol.
108 const NSCompositingOperation operation = NSCompositingOperationCopy;
109#else
110 const NSCompositingOperation operation = NSCompositeCopy;
111#endif
112 const NSSize cropped_size = NSMakeSize(image.size.width, (int)(image.size.height / frames));
113 NSImage *cropped = [[NSImage alloc] initWithSize:cropped_size];
114 if (cropped == nil) {
115 return [NSCursor performSelector:fallback];
116 }
117
118 [cropped lockFocus];
119 {
120 const NSRect cropped_rect = NSMakeRect(0, 0, cropped_size.width, cropped_size.height);
121 [image drawInRect:cropped_rect fromRect:cropped_rect operation:operation fraction:1];
122 }
123 [cropped unlockFocus];
124 image = cropped;
125 }
126
127 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint([[info valueForKey:@"hotx"] doubleValue], [[info valueForKey:@"hoty"] doubleValue])];
128 return cursor;
129}
130
131static SDL_Cursor *Cocoa_CreateSystemCursor(SDL_SystemCursor id)
132{
133 @autoreleasepool {
134 NSCursor *nscursor = NULL;
135 SDL_Cursor *cursor = NULL;
136
137 switch (id) {
138 case SDL_SYSTEM_CURSOR_DEFAULT:
139 nscursor = [NSCursor arrowCursor];
140 break;
141 case SDL_SYSTEM_CURSOR_TEXT:
142 nscursor = [NSCursor IBeamCursor];
143 break;
144 case SDL_SYSTEM_CURSOR_CROSSHAIR:
145 nscursor = [NSCursor crosshairCursor];
146 break;
147 case SDL_SYSTEM_CURSOR_WAIT: // !!! FIXME: this is more like WAITARROW
148 nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor));
149 break;
150 case SDL_SYSTEM_CURSOR_PROGRESS: // !!! FIXME: this is meant to be animated
151 nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor));
152 break;
153 case SDL_SYSTEM_CURSOR_NWSE_RESIZE:
154 nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor));
155 break;
156 case SDL_SYSTEM_CURSOR_NESW_RESIZE:
157 nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor));
158 break;
159 case SDL_SYSTEM_CURSOR_EW_RESIZE:
160 nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor));
161 break;
162 case SDL_SYSTEM_CURSOR_NS_RESIZE:
163 nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor));
164 break;
165 case SDL_SYSTEM_CURSOR_MOVE:
166 nscursor = LoadHiddenSystemCursor(@"move", @selector(closedHandCursor));
167 break;
168 case SDL_SYSTEM_CURSOR_NOT_ALLOWED:
169 nscursor = [NSCursor operationNotAllowedCursor];
170 break;
171 case SDL_SYSTEM_CURSOR_POINTER:
172 nscursor = [NSCursor pointingHandCursor];
173 break;
174 case SDL_SYSTEM_CURSOR_NW_RESIZE:
175 nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor));
176 break;
177 case SDL_SYSTEM_CURSOR_N_RESIZE:
178 nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor));
179 break;
180 case SDL_SYSTEM_CURSOR_NE_RESIZE:
181 nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor));
182 break;
183 case SDL_SYSTEM_CURSOR_E_RESIZE:
184 nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor));
185 break;
186 case SDL_SYSTEM_CURSOR_SE_RESIZE:
187 nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor));
188 break;
189 case SDL_SYSTEM_CURSOR_S_RESIZE:
190 nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor));
191 break;
192 case SDL_SYSTEM_CURSOR_SW_RESIZE:
193 nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor));
194 break;
195 case SDL_SYSTEM_CURSOR_W_RESIZE:
196 nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor));
197 break;
198 default:
199 SDL_assert(!"Unknown system cursor");
200 return NULL;
201 }
202
203 if (nscursor) {
204 cursor = SDL_calloc(1, sizeof(*cursor));
205 if (cursor) {
206 // We'll free it later, so retain it here
207 cursor->internal = (void *)CFBridgingRetain(nscursor);
208 }
209 }
210
211 return cursor;
212 }
213}
214
215static SDL_Cursor *Cocoa_CreateDefaultCursor(void)
216{
217 SDL_SystemCursor id = SDL_GetDefaultSystemCursor();
218 return Cocoa_CreateSystemCursor(id);
219}
220
221static void Cocoa_FreeCursor(SDL_Cursor *cursor)
222{
223 @autoreleasepool {
224 CFBridgingRelease((void *)cursor->internal);
225 SDL_free(cursor);
226 }
227}
228
229static bool Cocoa_ShowCursor(SDL_Cursor *cursor)
230{
231 @autoreleasepool {
232 SDL_VideoDevice *device = SDL_GetVideoDevice();
233 SDL_Window *window = (device ? device->windows : NULL);
234 for (; window != NULL; window = window->next) {
235 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
236 if (data) {
237 [data.nswindow performSelectorOnMainThread:@selector(invalidateCursorRectsForView:)
238 withObject:[data.nswindow contentView]
239 waitUntilDone:NO];
240 }
241 }
242 return true;
243 }
244}
245
246static SDL_Window *SDL_FindWindowAtPoint(const float x, const float y)
247{
248 const SDL_FPoint pt = { x, y };
249 SDL_Window *i;
250 for (i = SDL_GetVideoDevice()->windows; i; i = i->next) {
251 const SDL_FRect r = { (float)i->x, (float)i->y, (float)i->w, (float)i->h };
252 if (SDL_PointInRectFloat(&pt, &r)) {
253 return i;
254 }
255 }
256
257 return NULL;
258}
259
260static bool Cocoa_WarpMouseGlobal(float x, float y)
261{
262 CGPoint point;
263 SDL_Mouse *mouse = SDL_GetMouse();
264 if (mouse->focus) {
265 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)mouse->focus->internal;
266 if ([data.listener isMovingOrFocusClickPending]) {
267 DLog("Postponing warp, window being moved or focused.");
268 [data.listener setPendingMoveX:x Y:y];
269 return true;
270 }
271 }
272 point = CGPointMake(x, y);
273
274 Cocoa_HandleMouseWarp(point.x, point.y);
275
276 CGWarpMouseCursorPosition(point);
277
278 /* CGWarpMouse causes a short delay by default, which is preventable by
279 * Calling this directly after. CGSetLocalEventsSuppressionInterval can also
280 * prevent it, but it's deprecated as macOS 10.6.
281 */
282 if (!mouse->relative_mode) {
283 CGAssociateMouseAndMouseCursorPosition(YES);
284 }
285
286 /* CGWarpMouseCursorPosition doesn't generate a window event, unlike our
287 * other implementations' APIs. Send what's appropriate.
288 */
289 if (!mouse->relative_mode) {
290 SDL_Window *win = SDL_FindWindowAtPoint(x, y);
291 SDL_SetMouseFocus(win);
292 if (win) {
293 SDL_assert(win == mouse->focus);
294 SDL_SendMouseMotion(0, win, SDL_GLOBAL_MOUSE_ID, false, x - win->x, y - win->y);
295 }
296 }
297
298 return true;
299}
300
301static bool Cocoa_WarpMouse(SDL_Window *window, float x, float y)
302{
303 return Cocoa_WarpMouseGlobal(window->x + x, window->y + y);
304}
305
306static bool Cocoa_SetRelativeMouseMode(bool enabled)
307{
308 CGError result;
309
310 if (enabled) {
311 SDL_Window *window = SDL_GetKeyboardFocus();
312 if (window) {
313 /* We will re-apply the relative mode when the window finishes being moved,
314 * if it is being moved right now.
315 */
316 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
317 if ([data.listener isMovingOrFocusClickPending]) {
318 return true;
319 }
320
321 // make sure the mouse isn't at the corner of the window, as this can confuse things if macOS thinks a window resize is happening on the first click.
322 const CGPoint point = CGPointMake((float)(window->x + (window->w / 2)), (float)(window->y + (window->h / 2)));
323 Cocoa_HandleMouseWarp(point.x, point.y);
324 CGWarpMouseCursorPosition(point);
325 }
326 DLog("Turning on.");
327 result = CGAssociateMouseAndMouseCursorPosition(NO);
328 } else {
329 DLog("Turning off.");
330 result = CGAssociateMouseAndMouseCursorPosition(YES);
331 }
332 if (result != kCGErrorSuccess) {
333 return SDL_SetError("CGAssociateMouseAndMouseCursorPosition() failed");
334 }
335
336 /* The hide/unhide calls are redundant most of the time, but they fix
337 * https://bugzilla.libsdl.org/show_bug.cgi?id=2550
338 */
339 if (enabled) {
340 [NSCursor hide];
341 } else {
342 [NSCursor unhide];
343 }
344 return true;
345}
346
347static bool Cocoa_CaptureMouse(SDL_Window *window)
348{
349 /* our Cocoa event code already tracks the mouse outside the window,
350 so all we have to do here is say "okay" and do what we always do. */
351 return true;
352}
353
354static SDL_MouseButtonFlags Cocoa_GetGlobalMouseState(float *x, float *y)
355{
356 const NSUInteger cocoaButtons = [NSEvent pressedMouseButtons];
357 const NSPoint cocoaLocation = [NSEvent mouseLocation];
358 SDL_MouseButtonFlags result = 0;
359
360 *x = cocoaLocation.x;
361 *y = (CGDisplayPixelsHigh(kCGDirectMainDisplay) - cocoaLocation.y);
362
363 result |= (cocoaButtons & (1 << 0)) ? SDL_BUTTON_LMASK : 0;
364 result |= (cocoaButtons & (1 << 1)) ? SDL_BUTTON_RMASK : 0;
365 result |= (cocoaButtons & (1 << 2)) ? SDL_BUTTON_MMASK : 0;
366 result |= (cocoaButtons & (1 << 3)) ? SDL_BUTTON_X1MASK : 0;
367 result |= (cocoaButtons & (1 << 4)) ? SDL_BUTTON_X2MASK : 0;
368
369 return result;
370}
371
372bool Cocoa_InitMouse(SDL_VideoDevice *_this)
373{
374 NSPoint location;
375 SDL_Mouse *mouse = SDL_GetMouse();
376 SDL_MouseData *internal = (SDL_MouseData *)SDL_calloc(1, sizeof(SDL_MouseData));
377 if (internal == NULL) {
378 return false;
379 }
380
381 mouse->internal = internal;
382 mouse->CreateCursor = Cocoa_CreateCursor;
383 mouse->CreateSystemCursor = Cocoa_CreateSystemCursor;
384 mouse->ShowCursor = Cocoa_ShowCursor;
385 mouse->FreeCursor = Cocoa_FreeCursor;
386 mouse->WarpMouse = Cocoa_WarpMouse;
387 mouse->WarpMouseGlobal = Cocoa_WarpMouseGlobal;
388 mouse->SetRelativeMouseMode = Cocoa_SetRelativeMouseMode;
389 mouse->CaptureMouse = Cocoa_CaptureMouse;
390 mouse->GetGlobalMouseState = Cocoa_GetGlobalMouseState;
391
392 SDL_SetDefaultCursor(Cocoa_CreateDefaultCursor());
393
394 location = [NSEvent mouseLocation];
395 internal->lastMoveX = location.x;
396 internal->lastMoveY = location.y;
397 return true;
398}
399
400static void Cocoa_HandleTitleButtonEvent(SDL_VideoDevice *_this, NSEvent *event)
401{
402 SDL_Window *window;
403 NSWindow *nswindow = [event window];
404
405 /* You might land in this function before SDL_Init if showing a message box.
406 Don't dereference a NULL pointer if that happens. */
407 if (_this == NULL) {
408 return;
409 }
410
411 for (window = _this->windows; window; window = window->next) {
412 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
413 if (data && data.nswindow == nswindow) {
414 switch ([event type]) {
415 case NSEventTypeLeftMouseDown:
416 case NSEventTypeRightMouseDown:
417 case NSEventTypeOtherMouseDown:
418 [data.listener setFocusClickPending:[event buttonNumber]];
419 break;
420 case NSEventTypeLeftMouseUp:
421 case NSEventTypeRightMouseUp:
422 case NSEventTypeOtherMouseUp:
423 [data.listener clearFocusClickPending:[event buttonNumber]];
424 break;
425 default:
426 break;
427 }
428 break;
429 }
430 }
431}
432
433static NSWindow *Cocoa_MouseFocus;
434
435NSWindow *Cocoa_GetMouseFocus()
436{
437 return Cocoa_MouseFocus;
438}
439
440void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event)
441{
442 SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
443 SDL_Mouse *mouse;
444 SDL_MouseData *data;
445 NSPoint location;
446 CGFloat lastMoveX, lastMoveY;
447 float deltaX, deltaY;
448 bool seenWarp;
449
450 // All events except NSEventTypeMouseExited can only happen if the window
451 // has mouse focus, so we'll always set the focus even if we happen to miss
452 // NSEventTypeMouseEntered, which apparently happens if the window is
453 // created under the mouse on macOS 12.7
454 NSEventType event_type = [event type];
455 if (event_type == NSEventTypeMouseExited) {
456 Cocoa_MouseFocus = NULL;
457 } else {
458 Cocoa_MouseFocus = [event window];
459 }
460
461 switch (event_type) {
462 case NSEventTypeMouseEntered:
463 case NSEventTypeMouseExited:
464 // Focus is handled above
465 return;
466
467 case NSEventTypeMouseMoved:
468 case NSEventTypeLeftMouseDragged:
469 case NSEventTypeRightMouseDragged:
470 case NSEventTypeOtherMouseDragged:
471 break;
472
473 case NSEventTypeLeftMouseDown:
474 case NSEventTypeLeftMouseUp:
475 case NSEventTypeRightMouseDown:
476 case NSEventTypeRightMouseUp:
477 case NSEventTypeOtherMouseDown:
478 case NSEventTypeOtherMouseUp:
479 if ([event window]) {
480 NSRect windowRect = [[[event window] contentView] frame];
481 if (!NSMouseInRect([event locationInWindow], windowRect, NO)) {
482 Cocoa_HandleTitleButtonEvent(_this, event);
483 return;
484 }
485 }
486 return;
487
488 default:
489 // Ignore any other events.
490 return;
491 }
492
493 mouse = SDL_GetMouse();
494 data = (SDL_MouseData *)mouse->internal;
495 if (!data) {
496 return; // can happen when returning from fullscreen Space on shutdown
497 }
498
499 seenWarp = data->seenWarp;
500 data->seenWarp = NO;
501
502 location = [NSEvent mouseLocation];
503 lastMoveX = data->lastMoveX;
504 lastMoveY = data->lastMoveY;
505 data->lastMoveX = location.x;
506 data->lastMoveY = location.y;
507 DLog("Last seen mouse: (%g, %g)", location.x, location.y);
508
509 // Non-relative movement is handled in -[SDL3Cocoa_WindowListener mouseMoved:]
510 if (!mouse->relative_mode) {
511 return;
512 }
513
514 // Ignore events that aren't inside the client area (i.e. title bar.)
515 if ([event window]) {
516 NSRect windowRect = [[[event window] contentView] frame];
517 if (!NSMouseInRect([event locationInWindow], windowRect, NO)) {
518 return;
519 }
520 }
521
522 deltaX = [event deltaX];
523 deltaY = [event deltaY];
524
525 if (seenWarp) {
526 deltaX += (lastMoveX - data->lastWarpX);
527 deltaY += ((CGDisplayPixelsHigh(kCGDirectMainDisplay) - lastMoveY) - data->lastWarpY);
528
529 DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], [event deltaY], deltaX, deltaY);
530 }
531
532 SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]), mouse->focus, mouseID, true, deltaX, deltaY);
533}
534
535void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event)
536{
537 SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
538 SDL_MouseWheelDirection direction;
539 CGFloat x, y;
540
541 x = -[event deltaX];
542 y = [event deltaY];
543 direction = SDL_MOUSEWHEEL_NORMAL;
544
545 if ([event isDirectionInvertedFromDevice] == YES) {
546 direction = SDL_MOUSEWHEEL_FLIPPED;
547 }
548
549 /* For discrete scroll events from conventional mice, always send a full tick.
550 For continuous scroll events from trackpads, send fractional deltas for smoother scrolling. */
551 if (![event hasPreciseScrollingDeltas]) {
552 if (x > 0) {
553 x = SDL_ceil(x);
554 } else if (x < 0) {
555 x = SDL_floor(x);
556 }
557 if (y > 0) {
558 y = SDL_ceil(y);
559 } else if (y < 0) {
560 y = SDL_floor(y);
561 }
562 }
563
564 SDL_SendMouseWheel(Cocoa_GetEventTimestamp([event timestamp]), window, mouseID, x, y, direction);
565}
566
567void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y)
568{
569 /* This makes Cocoa_HandleMouseEvent ignore the delta caused by the warp,
570 * since it gets included in the next movement event.
571 */
572 SDL_MouseData *data = (SDL_MouseData *)SDL_GetMouse()->internal;
573 data->lastWarpX = x;
574 data->lastWarpY = y;
575 data->seenWarp = true;
576
577 DLog("(%g, %g)", x, y);
578}
579
580void Cocoa_QuitMouse(SDL_VideoDevice *_this)
581{
582 SDL_Mouse *mouse = SDL_GetMouse();
583 if (mouse) {
584 if (mouse->internal) {
585 SDL_free(mouse->internal);
586 mouse->internal = NULL;
587 }
588 }
589}
590
591#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.h
new file mode 100644
index 0000000..33d7b0e
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.h
@@ -0,0 +1,88 @@
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#ifndef SDL_cocoaopengl_h_
24#define SDL_cocoaopengl_h_
25
26#ifdef SDL_VIDEO_OPENGL_CGL
27
28#import <Cocoa/Cocoa.h>
29#import <QuartzCore/CVDisplayLink.h>
30
31// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it.
32#ifdef __clang__
33#pragma clang diagnostic push
34#pragma clang diagnostic ignored "-Wdeprecated-declarations"
35#endif
36
37struct SDL_GLDriverData
38{
39 int initialized;
40};
41
42@interface SDL3OpenGLContext : NSOpenGLContext
43{
44 SDL_AtomicInt dirty;
45 SDL_Window *window;
46 CVDisplayLinkRef displayLink;
47 @public
48 SDL_Mutex *swapIntervalMutex;
49 @public
50 SDL_Condition *swapIntervalCond;
51 @public
52 SDL_AtomicInt swapIntervalSetting;
53 @public
54 SDL_AtomicInt swapIntervalsPassed;
55}
56
57- (id)initWithFormat:(NSOpenGLPixelFormat *)format
58 shareContext:(NSOpenGLContext *)share;
59- (void)scheduleUpdate;
60- (void)updateIfNeeded;
61- (void)movedToNewScreen;
62- (void)setWindow:(SDL_Window *)window;
63- (SDL_Window *)window;
64- (void)explicitUpdate;
65- (void)cleanup;
66
67@property(retain, nonatomic) NSOpenGLPixelFormat *openglPixelFormat; // macOS 10.10 has -[NSOpenGLContext pixelFormat] but this handles older OS releases.
68
69@end
70
71// OpenGL functions
72extern bool Cocoa_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path);
73extern SDL_FunctionPointer Cocoa_GL_GetProcAddress(SDL_VideoDevice *_this, const char *proc);
74extern void Cocoa_GL_UnloadLibrary(SDL_VideoDevice *_this);
75extern SDL_GLContext Cocoa_GL_CreateContext(SDL_VideoDevice *_this, SDL_Window *window);
76extern bool Cocoa_GL_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context);
77extern bool Cocoa_GL_SetSwapInterval(SDL_VideoDevice *_this, int interval);
78extern bool Cocoa_GL_GetSwapInterval(SDL_VideoDevice *_this, int *interval);
79extern bool Cocoa_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window);
80extern bool Cocoa_GL_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context);
81
82#ifdef __clang__
83#pragma clang diagnostic pop
84#endif
85
86#endif // SDL_VIDEO_OPENGL_CGL
87
88#endif // SDL_cocoaopengl_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.m
new file mode 100644
index 0000000..34002ec
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.m
@@ -0,0 +1,559 @@
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// NSOpenGL implementation of SDL OpenGL support
24
25#ifdef SDL_VIDEO_OPENGL_CGL
26#include "SDL_cocoavideo.h"
27#include "SDL_cocoaopengl.h"
28#include "SDL_cocoaopengles.h"
29
30#include <OpenGL/CGLTypes.h>
31#include <OpenGL/OpenGL.h>
32#include <OpenGL/CGLRenderers.h>
33
34#include <SDL3/SDL_opengl.h>
35#include "../../SDL_hints_c.h"
36
37#define DEFAULT_OPENGL "/System/Library/Frameworks/OpenGL.framework/Libraries/libGL.dylib"
38
39// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it.
40#ifdef __clang__
41#pragma clang diagnostic push
42#pragma clang diagnostic ignored "-Wdeprecated-declarations"
43#endif
44
45// _Nullable is available starting Xcode 7
46#ifdef __has_feature
47#if __has_feature(nullability)
48#define HAS_FEATURE_NULLABLE
49#endif
50#endif
51#ifndef HAS_FEATURE_NULLABLE
52#define _Nullable
53#endif
54
55static bool SDL_opengl_async_dispatch = false;
56
57static void SDLCALL SDL_OpenGLAsyncDispatchChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
58{
59 SDL_opengl_async_dispatch = SDL_GetStringBoolean(hint, false);
60}
61
62static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
63{
64 SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)displayLinkContext;
65
66 // printf("DISPLAY LINK! %u\n", (unsigned int) SDL_GetTicks());
67 const int setting = SDL_GetAtomicInt(&nscontext->swapIntervalSetting);
68 if (setting != 0) { // nothing to do if vsync is disabled, don't even lock
69 SDL_LockMutex(nscontext->swapIntervalMutex);
70 SDL_AddAtomicInt(&nscontext->swapIntervalsPassed, 1);
71 SDL_SignalCondition(nscontext->swapIntervalCond);
72 SDL_UnlockMutex(nscontext->swapIntervalMutex);
73 }
74
75 return kCVReturnSuccess;
76}
77
78@implementation SDL3OpenGLContext : NSOpenGLContext
79
80- (id)initWithFormat:(NSOpenGLPixelFormat *)format
81 shareContext:(NSOpenGLContext *)share
82{
83 self = [super initWithFormat:format shareContext:share];
84 if (self) {
85 self.openglPixelFormat = format;
86 SDL_SetAtomicInt(&self->dirty, 0);
87 self->window = NULL;
88 SDL_SetAtomicInt(&self->swapIntervalSetting, 0);
89 SDL_SetAtomicInt(&self->swapIntervalsPassed, 0);
90 self->swapIntervalCond = SDL_CreateCondition();
91 self->swapIntervalMutex = SDL_CreateMutex();
92 if (!self->swapIntervalCond || !self->swapIntervalMutex) {
93 return nil;
94 }
95
96 // !!! FIXME: check return values.
97 CVDisplayLinkCreateWithActiveCGDisplays(&self->displayLink);
98 CVDisplayLinkSetOutputCallback(self->displayLink, &DisplayLinkCallback, (__bridge void *_Nullable)self);
99 CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(self->displayLink, [self CGLContextObj], [format CGLPixelFormatObj]);
100 CVDisplayLinkStart(displayLink);
101 }
102
103 SDL_AddHintCallback(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, SDL_OpenGLAsyncDispatchChanged, NULL);
104 return self;
105}
106
107- (void)movedToNewScreen
108{
109 if (self->displayLink) {
110 CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(self->displayLink, [self CGLContextObj], [[self openglPixelFormat] CGLPixelFormatObj]);
111 }
112}
113
114- (void)scheduleUpdate
115{
116 SDL_AddAtomicInt(&self->dirty, 1);
117}
118
119// This should only be called on the thread on which a user is using the context.
120- (void)updateIfNeeded
121{
122 const int value = SDL_SetAtomicInt(&self->dirty, 0);
123 if (value > 0) {
124 // We call the real underlying update here, since -[SDL3OpenGLContext update] just calls us.
125 [self explicitUpdate];
126 }
127}
128
129// This should only be called on the thread on which a user is using the context.
130- (void)update
131{
132 // This ensures that regular 'update' calls clear the atomic dirty flag.
133 [self scheduleUpdate];
134 [self updateIfNeeded];
135}
136
137// Updates the drawable for the contexts and manages related state.
138- (void)setWindow:(SDL_Window *)newWindow
139{
140 if (self->window) {
141 SDL_CocoaWindowData *oldwindowdata = (__bridge SDL_CocoaWindowData *)self->window->internal;
142
143 // Make sure to remove us from the old window's context list, or we'll get scheduled updates from it too.
144 NSMutableArray *contexts = oldwindowdata.nscontexts;
145 @synchronized(contexts) {
146 [contexts removeObject:self];
147 }
148 }
149
150 self->window = newWindow;
151
152 if (newWindow) {
153 SDL_CocoaWindowData *windowdata = (__bridge SDL_CocoaWindowData *)newWindow->internal;
154 NSView *contentview = windowdata.sdlContentView;
155
156 // Now sign up for scheduled updates for the new window.
157 NSMutableArray *contexts = windowdata.nscontexts;
158 @synchronized(contexts) {
159 [contexts addObject:self];
160 }
161
162 if ([self view] != contentview) {
163 if ([NSThread isMainThread]) {
164 [self setView:contentview];
165 } else {
166 dispatch_sync(dispatch_get_main_queue(), ^{
167 [self setView:contentview];
168 });
169 }
170 if (self == [NSOpenGLContext currentContext]) {
171 [self explicitUpdate];
172 } else {
173 [self scheduleUpdate];
174 }
175 }
176 } else {
177 if ([NSThread isMainThread]) {
178 [self setView:nil];
179 } else {
180 dispatch_sync(dispatch_get_main_queue(), ^{ [self setView:nil]; });
181 }
182 }
183}
184
185- (SDL_Window *)window
186{
187 return self->window;
188}
189
190- (void)explicitUpdate
191{
192 if ([NSThread isMainThread]) {
193 [super update];
194 } else {
195 if (SDL_opengl_async_dispatch) {
196 dispatch_async(dispatch_get_main_queue(), ^{
197 [super update];
198 });
199 } else {
200 dispatch_sync(dispatch_get_main_queue(), ^{
201 [super update];
202 });
203 }
204 }
205}
206
207- (void)cleanup
208{
209 [self setWindow:NULL];
210
211 SDL_RemoveHintCallback(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, SDL_OpenGLAsyncDispatchChanged, NULL);
212 if (self->displayLink) {
213 CVDisplayLinkRelease(self->displayLink);
214 self->displayLink = nil;
215 }
216 if (self->swapIntervalCond) {
217 SDL_DestroyCondition(self->swapIntervalCond);
218 self->swapIntervalCond = NULL;
219 }
220 if (self->swapIntervalMutex) {
221 SDL_DestroyMutex(self->swapIntervalMutex);
222 self->swapIntervalMutex = NULL;
223 }
224}
225
226@end
227
228bool Cocoa_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path)
229{
230 // Load the OpenGL library
231 if (path == NULL) {
232 path = SDL_GetHint(SDL_HINT_OPENGL_LIBRARY);
233 }
234 if (path == NULL) {
235 path = DEFAULT_OPENGL;
236 }
237 _this->gl_config.dll_handle = SDL_LoadObject(path);
238 if (!_this->gl_config.dll_handle) {
239 return false;
240 }
241 SDL_strlcpy(_this->gl_config.driver_path, path,
242 SDL_arraysize(_this->gl_config.driver_path));
243 return true;
244}
245
246SDL_FunctionPointer Cocoa_GL_GetProcAddress(SDL_VideoDevice *_this, const char *proc)
247{
248 return SDL_LoadFunction(_this->gl_config.dll_handle, proc);
249}
250
251void Cocoa_GL_UnloadLibrary(SDL_VideoDevice *_this)
252{
253 SDL_UnloadObject(_this->gl_config.dll_handle);
254 _this->gl_config.dll_handle = NULL;
255}
256
257SDL_GLContext Cocoa_GL_CreateContext(SDL_VideoDevice *_this, SDL_Window *window)
258{
259 @autoreleasepool {
260 SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
261 SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal;
262 NSOpenGLPixelFormatAttribute attr[32];
263 NSOpenGLPixelFormat *fmt;
264 SDL3OpenGLContext *context;
265 SDL_GLContext sdlcontext;
266 NSOpenGLContext *share_context = nil;
267 int i = 0;
268 const char *glversion;
269 int glversion_major;
270 int glversion_minor;
271 NSOpenGLPixelFormatAttribute profile;
272 int interval;
273 int opaque;
274
275 if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_ES) {
276#ifdef SDL_VIDEO_OPENGL_EGL
277 // Switch to EGL based functions
278 Cocoa_GL_UnloadLibrary(_this);
279 _this->GL_LoadLibrary = Cocoa_GLES_LoadLibrary;
280 _this->GL_GetProcAddress = Cocoa_GLES_GetProcAddress;
281 _this->GL_UnloadLibrary = Cocoa_GLES_UnloadLibrary;
282 _this->GL_CreateContext = Cocoa_GLES_CreateContext;
283 _this->GL_MakeCurrent = Cocoa_GLES_MakeCurrent;
284 _this->GL_SetSwapInterval = Cocoa_GLES_SetSwapInterval;
285 _this->GL_GetSwapInterval = Cocoa_GLES_GetSwapInterval;
286 _this->GL_SwapWindow = Cocoa_GLES_SwapWindow;
287 _this->GL_DestroyContext = Cocoa_GLES_DestroyContext;
288
289 if (!Cocoa_GLES_LoadLibrary(_this, NULL)) {
290 return NULL;
291 }
292 return Cocoa_GLES_CreateContext(_this, window);
293#else
294 SDL_SetError("SDL not configured with EGL support");
295 return NULL;
296#endif
297 }
298
299 attr[i++] = NSOpenGLPFAAllowOfflineRenderers;
300
301 profile = NSOpenGLProfileVersionLegacy;
302 if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_CORE) {
303 profile = NSOpenGLProfileVersion3_2Core;
304 }
305 attr[i++] = NSOpenGLPFAOpenGLProfile;
306 attr[i++] = profile;
307
308 attr[i++] = NSOpenGLPFAColorSize;
309 attr[i++] = SDL_BYTESPERPIXEL(display->current_mode->format) * 8;
310
311 attr[i++] = NSOpenGLPFADepthSize;
312 attr[i++] = _this->gl_config.depth_size;
313
314 if (_this->gl_config.double_buffer) {
315 attr[i++] = NSOpenGLPFADoubleBuffer;
316 }
317
318 if (_this->gl_config.stereo) {
319 attr[i++] = NSOpenGLPFAStereo;
320 }
321
322 if (_this->gl_config.stencil_size) {
323 attr[i++] = NSOpenGLPFAStencilSize;
324 attr[i++] = _this->gl_config.stencil_size;
325 }
326
327 if ((_this->gl_config.accum_red_size +
328 _this->gl_config.accum_green_size +
329 _this->gl_config.accum_blue_size +
330 _this->gl_config.accum_alpha_size) > 0) {
331 attr[i++] = NSOpenGLPFAAccumSize;
332 attr[i++] = _this->gl_config.accum_red_size + _this->gl_config.accum_green_size + _this->gl_config.accum_blue_size + _this->gl_config.accum_alpha_size;
333 }
334
335 if (_this->gl_config.multisamplebuffers) {
336 attr[i++] = NSOpenGLPFASampleBuffers;
337 attr[i++] = _this->gl_config.multisamplebuffers;
338 }
339
340 if (_this->gl_config.multisamplesamples) {
341 attr[i++] = NSOpenGLPFASamples;
342 attr[i++] = _this->gl_config.multisamplesamples;
343 attr[i++] = NSOpenGLPFANoRecovery;
344 }
345 if (_this->gl_config.floatbuffers) {
346 attr[i++] = NSOpenGLPFAColorFloat;
347 }
348
349 if (_this->gl_config.accelerated >= 0) {
350 if (_this->gl_config.accelerated) {
351 attr[i++] = NSOpenGLPFAAccelerated;
352 } else {
353 attr[i++] = NSOpenGLPFARendererID;
354 attr[i++] = kCGLRendererGenericFloatID;
355 }
356 }
357
358 attr[i++] = NSOpenGLPFAScreenMask;
359 attr[i++] = CGDisplayIDToOpenGLDisplayMask(displaydata->display);
360 attr[i] = 0;
361
362 fmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
363 if (fmt == nil) {
364 SDL_SetError("Failed creating OpenGL pixel format");
365 return NULL;
366 }
367
368 if (_this->gl_config.share_with_current_context) {
369 share_context = (__bridge NSOpenGLContext *)SDL_GL_GetCurrentContext();
370 }
371
372 context = [[SDL3OpenGLContext alloc] initWithFormat:fmt shareContext:share_context];
373
374 if (context == nil) {
375 SDL_SetError("Failed creating OpenGL context");
376 return NULL;
377 }
378
379 sdlcontext = (SDL_GLContext)CFBridgingRetain(context);
380
381 // vsync is handled separately by synchronizing with a display link.
382 interval = 0;
383 [context setValues:&interval forParameter:NSOpenGLCPSwapInterval];
384
385 opaque = (window->flags & SDL_WINDOW_TRANSPARENT) ? 0 : 1;
386 [context setValues:&opaque forParameter:NSOpenGLCPSurfaceOpacity];
387
388 if (!Cocoa_GL_MakeCurrent(_this, window, sdlcontext)) {
389 SDL_GL_DestroyContext(sdlcontext);
390 SDL_SetError("Failed making OpenGL context current");
391 return NULL;
392 }
393
394 if (_this->gl_config.major_version < 3 &&
395 _this->gl_config.profile_mask == 0 &&
396 _this->gl_config.flags == 0) {
397 // This is a legacy profile, so to match other backends, we're done.
398 } else {
399 const GLubyte *(APIENTRY * glGetStringFunc)(GLenum);
400
401 glGetStringFunc = (const GLubyte *(APIENTRY *)(GLenum))SDL_GL_GetProcAddress("glGetString");
402 if (!glGetStringFunc) {
403 SDL_GL_DestroyContext(sdlcontext);
404 SDL_SetError("Failed getting OpenGL glGetString entry point");
405 return NULL;
406 }
407
408 glversion = (const char *)glGetStringFunc(GL_VERSION);
409 if (glversion == NULL) {
410 SDL_GL_DestroyContext(sdlcontext);
411 SDL_SetError("Failed getting OpenGL context version");
412 return NULL;
413 }
414
415 if (SDL_sscanf(glversion, "%d.%d", &glversion_major, &glversion_minor) != 2) {
416 SDL_GL_DestroyContext(sdlcontext);
417 SDL_SetError("Failed parsing OpenGL context version");
418 return NULL;
419 }
420
421 if ((glversion_major < _this->gl_config.major_version) ||
422 ((glversion_major == _this->gl_config.major_version) && (glversion_minor < _this->gl_config.minor_version))) {
423 SDL_GL_DestroyContext(sdlcontext);
424 SDL_SetError("Failed creating OpenGL context at version requested");
425 return NULL;
426 }
427
428 /* In the future we'll want to do this, but to match other platforms
429 we'll leave the OpenGL version the way it is for now
430 */
431 // _this->gl_config.major_version = glversion_major;
432 // _this->gl_config.minor_version = glversion_minor;
433 }
434 return sdlcontext;
435 }
436}
437
438bool Cocoa_GL_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context)
439{
440 @autoreleasepool {
441 if (context) {
442 SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)context;
443 if ([nscontext window] != window) {
444 [nscontext setWindow:window];
445 [nscontext updateIfNeeded];
446 }
447 [nscontext makeCurrentContext];
448 } else {
449 [NSOpenGLContext clearCurrentContext];
450 }
451
452 return true;
453 }
454}
455
456bool Cocoa_GL_SetSwapInterval(SDL_VideoDevice *_this, int interval)
457{
458 @autoreleasepool {
459 SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)SDL_GL_GetCurrentContext();
460 bool result;
461
462 if (nscontext == nil) {
463 result = SDL_SetError("No current OpenGL context");
464 } else {
465 SDL_LockMutex(nscontext->swapIntervalMutex);
466 SDL_SetAtomicInt(&nscontext->swapIntervalsPassed, 0);
467 SDL_SetAtomicInt(&nscontext->swapIntervalSetting, interval);
468 SDL_UnlockMutex(nscontext->swapIntervalMutex);
469 result = true;
470 }
471
472 return result;
473 }
474}
475
476bool Cocoa_GL_GetSwapInterval(SDL_VideoDevice *_this, int *interval)
477{
478 @autoreleasepool {
479 SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)SDL_GL_GetCurrentContext();
480 if (nscontext) {
481 *interval = SDL_GetAtomicInt(&nscontext->swapIntervalSetting);
482 return true;
483 } else {
484 return SDL_SetError("no OpenGL context");
485 }
486 }
487}
488
489bool Cocoa_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window)
490{
491 @autoreleasepool {
492 SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)SDL_GL_GetCurrentContext();
493 SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal;
494 const int setting = SDL_GetAtomicInt(&nscontext->swapIntervalSetting);
495
496 if (setting == 0) {
497 // nothing to do if vsync is disabled, don't even lock
498 } else if (setting < 0) { // late swap tearing
499 SDL_LockMutex(nscontext->swapIntervalMutex);
500 while (SDL_GetAtomicInt(&nscontext->swapIntervalsPassed) == 0) {
501 SDL_WaitCondition(nscontext->swapIntervalCond, nscontext->swapIntervalMutex);
502 }
503 SDL_SetAtomicInt(&nscontext->swapIntervalsPassed, 0);
504 SDL_UnlockMutex(nscontext->swapIntervalMutex);
505 } else {
506 SDL_LockMutex(nscontext->swapIntervalMutex);
507 do { // always wait here so we know we just hit a swap interval.
508 SDL_WaitCondition(nscontext->swapIntervalCond, nscontext->swapIntervalMutex);
509 } while ((SDL_GetAtomicInt(&nscontext->swapIntervalsPassed) % setting) != 0);
510 SDL_SetAtomicInt(&nscontext->swapIntervalsPassed, 0);
511 SDL_UnlockMutex(nscontext->swapIntervalMutex);
512 }
513
514 // { static Uint64 prev = 0; const Uint64 now = SDL_GetTicks(); const unsigned int diff = (unsigned int) (now - prev); prev = now; printf("GLSWAPBUFFERS TICKS %u\n", diff); }
515
516 /* on 10.14 ("Mojave") and later, this deadlocks if two contexts in two
517 threads try to swap at the same time, so put a mutex around it. */
518 SDL_LockMutex(videodata.swaplock);
519 [nscontext flushBuffer];
520 [nscontext updateIfNeeded];
521 SDL_UnlockMutex(videodata.swaplock);
522 return true;
523 }
524}
525
526static void DispatchedDestroyContext(SDL_GLContext context)
527{
528 @autoreleasepool {
529 SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)context;
530 [nscontext cleanup];
531 CFRelease(context);
532 }
533}
534
535bool Cocoa_GL_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context)
536{
537 if ([NSThread isMainThread]) {
538 DispatchedDestroyContext(context);
539 } else {
540 if (SDL_opengl_async_dispatch) {
541 dispatch_async(dispatch_get_main_queue(), ^{
542 DispatchedDestroyContext(context);
543 });
544 } else {
545 dispatch_sync(dispatch_get_main_queue(), ^{
546 DispatchedDestroyContext(context);
547 });
548 }
549 }
550
551 return true;
552}
553
554// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it.
555#ifdef __clang__
556#pragma clang diagnostic pop
557#endif
558
559#endif // SDL_VIDEO_OPENGL_CGL
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.h
new file mode 100644
index 0000000..5cf97e3
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.h
@@ -0,0 +1,48 @@
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#ifndef SDL_cocoaopengles_h_
24#define SDL_cocoaopengles_h_
25
26#ifdef SDL_VIDEO_OPENGL_EGL
27
28#include "../SDL_sysvideo.h"
29#include "../SDL_egl_c.h"
30
31// OpenGLES functions
32#define Cocoa_GLES_GetAttribute SDL_EGL_GetAttribute
33#define Cocoa_GLES_GetProcAddress SDL_EGL_GetProcAddressInternal
34#define Cocoa_GLES_UnloadLibrary SDL_EGL_UnloadLibrary
35#define Cocoa_GLES_GetSwapInterval SDL_EGL_GetSwapInterval
36#define Cocoa_GLES_SetSwapInterval SDL_EGL_SetSwapInterval
37
38extern bool Cocoa_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path);
39extern SDL_GLContext Cocoa_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window);
40extern bool Cocoa_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window);
41extern bool Cocoa_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context);
42extern bool Cocoa_GLES_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context);
43extern bool Cocoa_GLES_SetupWindow(SDL_VideoDevice *_this, SDL_Window *window);
44extern SDL_EGLSurface Cocoa_GLES_GetEGLSurface(SDL_VideoDevice *_this, SDL_Window *window);
45
46#endif // SDL_VIDEO_OPENGL_EGL
47
48#endif // SDL_cocoaopengles_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.m
new file mode 100644
index 0000000..053ddc9
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.m
@@ -0,0 +1,156 @@
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#if defined(SDL_VIDEO_DRIVER_COCOA) && defined(SDL_VIDEO_OPENGL_EGL)
24
25#include "SDL_cocoavideo.h"
26#include "SDL_cocoaopengles.h"
27#include "SDL_cocoaopengl.h"
28
29// EGL implementation of SDL OpenGL support
30
31bool Cocoa_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path)
32{
33 // If the profile requested is not GL ES, switch over to WIN_GL functions
34 if (_this->gl_config.profile_mask != SDL_GL_CONTEXT_PROFILE_ES) {
35#ifdef SDL_VIDEO_OPENGL_CGL
36 Cocoa_GLES_UnloadLibrary(_this);
37 _this->GL_LoadLibrary = Cocoa_GL_LoadLibrary;
38 _this->GL_GetProcAddress = Cocoa_GL_GetProcAddress;
39 _this->GL_UnloadLibrary = Cocoa_GL_UnloadLibrary;
40 _this->GL_CreateContext = Cocoa_GL_CreateContext;
41 _this->GL_MakeCurrent = Cocoa_GL_MakeCurrent;
42 _this->GL_SetSwapInterval = Cocoa_GL_SetSwapInterval;
43 _this->GL_GetSwapInterval = Cocoa_GL_GetSwapInterval;
44 _this->GL_SwapWindow = Cocoa_GL_SwapWindow;
45 _this->GL_DestroyContext = Cocoa_GL_DestroyContext;
46 _this->GL_GetEGLSurface = NULL;
47 return Cocoa_GL_LoadLibrary(_this, path);
48#else
49 return SDL_SetError("SDL not configured with OpenGL/CGL support");
50#endif
51 }
52
53 if (_this->egl_data == NULL) {
54 return SDL_EGL_LoadLibrary(_this, NULL, EGL_DEFAULT_DISPLAY, _this->gl_config.egl_platform);
55 }
56
57 return true;
58}
59
60SDL_GLContext Cocoa_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window)
61{
62 @autoreleasepool {
63 SDL_GLContext context;
64 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
65
66#ifdef SDL_VIDEO_OPENGL_CGL
67 if (_this->gl_config.profile_mask != SDL_GL_CONTEXT_PROFILE_ES) {
68 // Switch to CGL based functions
69 Cocoa_GLES_UnloadLibrary(_this);
70 _this->GL_LoadLibrary = Cocoa_GL_LoadLibrary;
71 _this->GL_GetProcAddress = Cocoa_GL_GetProcAddress;
72 _this->GL_UnloadLibrary = Cocoa_GL_UnloadLibrary;
73 _this->GL_CreateContext = Cocoa_GL_CreateContext;
74 _this->GL_MakeCurrent = Cocoa_GL_MakeCurrent;
75 _this->GL_SetSwapInterval = Cocoa_GL_SetSwapInterval;
76 _this->GL_GetSwapInterval = Cocoa_GL_GetSwapInterval;
77 _this->GL_SwapWindow = Cocoa_GL_SwapWindow;
78 _this->GL_DestroyContext = Cocoa_GL_DestroyContext;
79 _this->GL_GetEGLSurface = NULL;
80
81 if (!Cocoa_GL_LoadLibrary(_this, NULL)) {
82 return NULL;
83 }
84
85 return Cocoa_GL_CreateContext(_this, window);
86 }
87#endif
88
89 context = SDL_EGL_CreateContext(_this, data.egl_surface);
90 return context;
91 }
92}
93
94bool Cocoa_GLES_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context)
95{
96 @autoreleasepool {
97 SDL_EGL_DestroyContext(_this, context);
98 }
99 return true;
100}
101
102bool Cocoa_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window)
103{
104 @autoreleasepool {
105 return SDL_EGL_SwapBuffers(_this, ((__bridge SDL_CocoaWindowData *)window->internal).egl_surface);
106 }
107}
108
109bool Cocoa_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context)
110{
111 @autoreleasepool {
112 return SDL_EGL_MakeCurrent(_this, window ? ((__bridge SDL_CocoaWindowData *)window->internal).egl_surface : EGL_NO_SURFACE, context);
113 }
114}
115
116bool Cocoa_GLES_SetupWindow(SDL_VideoDevice *_this, SDL_Window *window)
117{
118 @autoreleasepool {
119 NSView *v;
120 // The current context is lost in here; save it and reset it.
121 SDL_CocoaWindowData *windowdata = (__bridge SDL_CocoaWindowData *)window->internal;
122 SDL_Window *current_win = SDL_GL_GetCurrentWindow();
123 SDL_GLContext current_ctx = SDL_GL_GetCurrentContext();
124
125 if (_this->egl_data == NULL) {
126// !!! FIXME: commenting out this assertion is (I think) incorrect; figure out why driver_loaded is wrong for ANGLE instead. --ryan.
127#if 0 // When hint SDL_HINT_OPENGL_ES_DRIVER is set to "1" (e.g. for ANGLE support), _this->gl_config.driver_loaded can be 1, while the below lines function.
128 SDL_assert(!_this->gl_config.driver_loaded);
129#endif
130 if (!SDL_EGL_LoadLibrary(_this, NULL, EGL_DEFAULT_DISPLAY, _this->gl_config.egl_platform)) {
131 SDL_EGL_UnloadLibrary(_this);
132 return false;
133 }
134 _this->gl_config.driver_loaded = 1;
135 }
136
137 // Create the GLES window surface
138 v = windowdata.nswindow.contentView;
139 windowdata.egl_surface = SDL_EGL_CreateSurface(_this, window, (__bridge NativeWindowType)[v layer]);
140
141 if (windowdata.egl_surface == EGL_NO_SURFACE) {
142 return SDL_SetError("Could not create GLES window surface");
143 }
144
145 return Cocoa_GLES_MakeCurrent(_this, current_win, current_ctx);
146 }
147}
148
149SDL_EGLSurface Cocoa_GLES_GetEGLSurface(SDL_VideoDevice *_this, SDL_Window *window)
150{
151 @autoreleasepool {
152 return ((__bridge SDL_CocoaWindowData *)window->internal).egl_surface;
153 }
154}
155
156#endif // SDL_VIDEO_DRIVER_COCOA && SDL_VIDEO_OPENGL_EGL
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.h
new file mode 100644
index 0000000..b659ba4
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.h
@@ -0,0 +1,32 @@
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#ifndef SDL_cocoapen_h_
24#define SDL_cocoapenm_h_
25
26#include "SDL_cocoavideo.h"
27
28extern bool Cocoa_InitPen(SDL_VideoDevice *_this);
29extern bool Cocoa_HandlePenEvent(SDL_CocoaWindowData *_data, NSEvent *event); // return false if we didn't handle this event.
30extern void Cocoa_QuitPen(SDL_VideoDevice *_this);
31
32#endif // SDL_cocoapen_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.m
new file mode 100644
index 0000000..6c30bfb
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.m
@@ -0,0 +1,178 @@
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 "SDL_cocoapen.h"
26#include "SDL_cocoavideo.h"
27
28#include "../../events/SDL_pen_c.h"
29
30bool Cocoa_InitPen(SDL_VideoDevice *_this)
31{
32 return true;
33}
34
35typedef struct Cocoa_PenHandle
36{
37 NSUInteger deviceid;
38 NSUInteger toolid;
39 SDL_PenID pen;
40 bool is_eraser;
41} Cocoa_PenHandle;
42
43typedef struct FindPenByDeviceAndToolIDData
44{
45 NSUInteger deviceid;
46 NSUInteger toolid;
47 void *handle;
48} FindPenByDeviceAndToolIDData;
49
50static bool FindPenByDeviceAndToolID(void *handle, void *userdata)
51{
52 const Cocoa_PenHandle *cocoa_handle = (const Cocoa_PenHandle *) handle;
53 FindPenByDeviceAndToolIDData *data = (FindPenByDeviceAndToolIDData *) userdata;
54
55 if (cocoa_handle->deviceid != data->deviceid) {
56 return false;
57 } else if (cocoa_handle->toolid != data->toolid) {
58 return false;
59 }
60 data->handle = handle;
61 return true;
62}
63
64static Cocoa_PenHandle *Cocoa_FindPenByDeviceID(NSUInteger deviceid, NSUInteger toolid)
65{
66 FindPenByDeviceAndToolIDData data;
67 data.deviceid = deviceid;
68 data.toolid = toolid;
69 data.handle = NULL;
70 SDL_FindPenByCallback(FindPenByDeviceAndToolID, &data);
71 return (Cocoa_PenHandle *) data.handle;
72}
73
74static void Cocoa_HandlePenProximityEvent(SDL_CocoaWindowData *_data, NSEvent *event)
75{
76 const NSUInteger devid = [event deviceID];
77 const NSUInteger toolid = [event pointingDeviceID];
78
79 if (event.enteringProximity) { // new pen coming!
80 const NSPointingDeviceType devtype = [event pointingDeviceType];
81 const bool is_eraser = (devtype == NSPointingDeviceTypeEraser);
82 const bool is_pen = (devtype == NSPointingDeviceTypePen);
83 if (!is_eraser && !is_pen) {
84 return; // we ignore other things, which hopefully is right.
85 }
86
87 Cocoa_PenHandle *handle = (Cocoa_PenHandle *) SDL_calloc(1, sizeof (*handle));
88 if (!handle) {
89 return; // oh well.
90 }
91
92 // Cocoa offers almost none of this information as specifics, but can without warning offer any of these specific things.
93 SDL_PenInfo peninfo;
94 SDL_zero(peninfo);
95 peninfo.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_ROTATION | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT | SDL_PEN_CAPABILITY_TANGENTIAL_PRESSURE | (is_eraser ? SDL_PEN_CAPABILITY_ERASER : 0);
96 peninfo.max_tilt = 90.0f;
97 peninfo.num_buttons = 2;
98 peninfo.subtype = is_eraser ? SDL_PEN_TYPE_ERASER : SDL_PEN_TYPE_PEN;
99
100 handle->deviceid = devid;
101 handle->toolid = toolid;
102 handle->is_eraser = is_eraser;
103 handle->pen = SDL_AddPenDevice(Cocoa_GetEventTimestamp([event timestamp]), NULL, &peninfo, handle);
104 if (!handle->pen) {
105 SDL_free(handle); // oh well.
106 }
107 } else { // old pen leaving!
108 Cocoa_PenHandle *handle = Cocoa_FindPenByDeviceID(devid, toolid);
109 if (handle) {
110 SDL_RemovePenDevice(Cocoa_GetEventTimestamp([event timestamp]), handle->pen);
111 SDL_free(handle);
112 }
113 }
114}
115
116static void Cocoa_HandlePenPointEvent(SDL_CocoaWindowData *_data, NSEvent *event)
117{
118 const Uint64 timestamp = Cocoa_GetEventTimestamp([event timestamp]);
119 Cocoa_PenHandle *handle = Cocoa_FindPenByDeviceID([event deviceID], [event pointingDeviceID]);
120 if (!handle) {
121 return;
122 }
123
124 const SDL_PenID pen = handle->pen;
125 const NSEventButtonMask buttons = [event buttonMask];
126 const NSPoint tilt = [event tilt];
127 const NSPoint point = [event locationInWindow];
128 const bool is_touching = (buttons & NSEventButtonMaskPenTip) != 0;
129 SDL_Window *window = _data.window;
130
131 SDL_SendPenTouch(timestamp, pen, window, handle->is_eraser, is_touching);
132 SDL_SendPenMotion(timestamp, pen, window, (float) point.x, (float) (window->h - point.y));
133 SDL_SendPenButton(timestamp, pen, window, 1, ((buttons & NSEventButtonMaskPenLowerSide) != 0));
134 SDL_SendPenButton(timestamp, pen, window, 2, ((buttons & NSEventButtonMaskPenUpperSide) != 0));
135 SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_PRESSURE, [event pressure]);
136 SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_ROTATION, [event rotation]);
137 SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_XTILT, ((float) tilt.x) * 90.0f);
138 SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_YTILT, ((float) -tilt.y) * 90.0f);
139 SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_TANGENTIAL_PRESSURE, event.tangentialPressure);
140}
141
142bool Cocoa_HandlePenEvent(SDL_CocoaWindowData *_data, NSEvent *event)
143{
144 NSEventType type = [event type];
145
146 if ((type != NSEventTypeTabletPoint) && (type != NSEventTypeTabletProximity)) {
147 const NSEventSubtype subtype = [event subtype];
148 if (subtype == NSEventSubtypeTabletPoint) {
149 type = NSEventTypeTabletPoint;
150 } else if (subtype == NSEventSubtypeTabletProximity) {
151 type = NSEventTypeTabletProximity;
152 } else {
153 return false; // not a tablet event.
154 }
155 }
156
157 if (type == NSEventTypeTabletPoint) {
158 Cocoa_HandlePenPointEvent(_data, event);
159 } else if (type == NSEventTypeTabletProximity) {
160 Cocoa_HandlePenProximityEvent(_data, event);
161 } else {
162 return false; // not a tablet event.
163 }
164
165 return true;
166}
167
168static void Cocoa_FreePenHandle(SDL_PenID instance_id, void *handle, void *userdata)
169{
170 SDL_free(handle);
171}
172
173void Cocoa_QuitPen(SDL_VideoDevice *_this)
174{
175 SDL_RemoveAllPenDevices(Cocoa_FreePenHandle, NULL);
176}
177
178#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.h
new file mode 100644
index 0000000..9ca3c64
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.h
@@ -0,0 +1,28 @@
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#ifndef SDL_cocoashape_h_
24#define SDL_cocoashape_h_
25
26extern bool Cocoa_UpdateWindowShape(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *shape);
27
28#endif // SDL_cocoashape_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.m
new file mode 100644
index 0000000..26081bd
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.m
@@ -0,0 +1,54 @@
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 "SDL_cocoavideo.h"
26#include "SDL_cocoashape.h"
27
28
29bool Cocoa_UpdateWindowShape(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *shape)
30{
31 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
32 BOOL ignoresMouseEvents = NO;
33
34 if (shape) {
35 SDL_FPoint point;
36 SDL_GetGlobalMouseState(&point.x, &point.y);
37 point.x -= window->x;
38 point.y -= window->y;
39 if (point.x >= 0.0f && point.x < window->w &&
40 point.y >= 0.0f && point.y < window->h) {
41 int x = (int)SDL_roundf((point.x / (window->w - 1)) * (shape->w - 1));
42 int y = (int)SDL_roundf((point.y / (window->h - 1)) * (shape->h - 1));
43 Uint8 a;
44
45 if (!SDL_ReadSurfacePixel(shape, x, y, NULL, NULL, NULL, &a) || a == SDL_ALPHA_TRANSPARENT) {
46 ignoresMouseEvents = YES;
47 }
48 }
49 }
50 data.nswindow.ignoresMouseEvents = ignoresMouseEvents;
51 return true;
52}
53
54#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.h
new file mode 100644
index 0000000..353fb43
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.h
@@ -0,0 +1,71 @@
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#ifndef SDL_cocoavideo_h_
24#define SDL_cocoavideo_h_
25
26#include <SDL3/SDL_opengl.h>
27
28#include <ApplicationServices/ApplicationServices.h>
29#include <IOKit/pwr_mgt/IOPMLib.h>
30#include <Cocoa/Cocoa.h>
31
32#include "../SDL_sysvideo.h"
33
34#include "SDL_cocoaclipboard.h"
35#include "SDL_cocoaevents.h"
36#include "SDL_cocoakeyboard.h"
37#include "SDL_cocoamodes.h"
38#include "SDL_cocoamouse.h"
39#include "SDL_cocoaopengl.h"
40#include "SDL_cocoawindow.h"
41#include "SDL_cocoapen.h"
42
43// Private display data
44
45@class SDL3TranslatorResponder;
46
47typedef enum
48{
49 OptionAsAltNone,
50 OptionAsAltOnlyLeft,
51 OptionAsAltOnlyRight,
52 OptionAsAltBoth,
53} OptionAsAlt;
54
55@interface SDL_CocoaVideoData : NSObject
56@property(nonatomic) int allow_spaces;
57@property(nonatomic) int trackpad_is_touch_only;
58@property(nonatomic) unsigned int modifierFlags;
59@property(nonatomic) void *key_layout;
60@property(nonatomic) SDL3TranslatorResponder *fieldEdit;
61@property(nonatomic) NSInteger clipboard_count;
62@property(nonatomic) IOPMAssertionID screensaver_assertion;
63@property(nonatomic) SDL_Mutex *swaplock;
64@property(nonatomic) OptionAsAlt option_as_alt;
65@end
66
67// Utility functions
68extern SDL_SystemTheme Cocoa_GetSystemTheme(void);
69extern NSImage *Cocoa_CreateImage(SDL_Surface *surface);
70
71#endif // SDL_cocoavideo_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.m
new file mode 100644
index 0000000..81baf78
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.m
@@ -0,0 +1,337 @@
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#if !__has_feature(objc_arc)
26#error SDL must be built with Objective-C ARC (automatic reference counting) enabled
27#endif
28
29#include "SDL_cocoavideo.h"
30#include "SDL_cocoavulkan.h"
31#include "SDL_cocoametalview.h"
32#include "SDL_cocoaopengles.h"
33#include "SDL_cocoamessagebox.h"
34#include "SDL_cocoashape.h"
35
36#include "../../events/SDL_keyboard_c.h"
37#include "../../events/SDL_mouse_c.h"
38
39@implementation SDL_CocoaVideoData
40
41@end
42
43// Initialization/Query functions
44static bool Cocoa_VideoInit(SDL_VideoDevice *_this);
45static void Cocoa_VideoQuit(SDL_VideoDevice *_this);
46
47// Cocoa driver bootstrap functions
48
49static void Cocoa_DeleteDevice(SDL_VideoDevice *device)
50{
51 @autoreleasepool {
52 if (device->wakeup_lock) {
53 SDL_DestroyMutex(device->wakeup_lock);
54 }
55 CFBridgingRelease(device->internal);
56 SDL_free(device);
57 }
58}
59
60static SDL_VideoDevice *Cocoa_CreateDevice(void)
61{
62 @autoreleasepool {
63 SDL_VideoDevice *device;
64 SDL_CocoaVideoData *data;
65
66 if (![NSThread isMainThread]) {
67 return NULL; // this doesn't SDL_SetError() because SDL_VideoInit is just going to overwrite it.
68 }
69
70 Cocoa_RegisterApp();
71
72 // Initialize all variables that we clean on shutdown
73 device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice));
74 if (device) {
75 data = [[SDL_CocoaVideoData alloc] init];
76 } else {
77 data = nil;
78 }
79 if (!data) {
80 SDL_free(device);
81 return NULL;
82 }
83 device->internal = (SDL_VideoData *)CFBridgingRetain(data);
84 device->wakeup_lock = SDL_CreateMutex();
85 device->system_theme = Cocoa_GetSystemTheme();
86
87 // Set the function pointers
88 device->VideoInit = Cocoa_VideoInit;
89 device->VideoQuit = Cocoa_VideoQuit;
90 device->GetDisplayBounds = Cocoa_GetDisplayBounds;
91 device->GetDisplayUsableBounds = Cocoa_GetDisplayUsableBounds;
92 device->GetDisplayModes = Cocoa_GetDisplayModes;
93 device->SetDisplayMode = Cocoa_SetDisplayMode;
94 device->PumpEvents = Cocoa_PumpEvents;
95 device->WaitEventTimeout = Cocoa_WaitEventTimeout;
96 device->SendWakeupEvent = Cocoa_SendWakeupEvent;
97 device->SuspendScreenSaver = Cocoa_SuspendScreenSaver;
98
99 device->CreateSDLWindow = Cocoa_CreateWindow;
100 device->SetWindowTitle = Cocoa_SetWindowTitle;
101 device->SetWindowIcon = Cocoa_SetWindowIcon;
102 device->SetWindowPosition = Cocoa_SetWindowPosition;
103 device->SetWindowSize = Cocoa_SetWindowSize;
104 device->SetWindowMinimumSize = Cocoa_SetWindowMinimumSize;
105 device->SetWindowMaximumSize = Cocoa_SetWindowMaximumSize;
106 device->SetWindowAspectRatio = Cocoa_SetWindowAspectRatio;
107 device->SetWindowOpacity = Cocoa_SetWindowOpacity;
108 device->GetWindowSizeInPixels = Cocoa_GetWindowSizeInPixels;
109 device->ShowWindow = Cocoa_ShowWindow;
110 device->HideWindow = Cocoa_HideWindow;
111 device->RaiseWindow = Cocoa_RaiseWindow;
112 device->MaximizeWindow = Cocoa_MaximizeWindow;
113 device->MinimizeWindow = Cocoa_MinimizeWindow;
114 device->RestoreWindow = Cocoa_RestoreWindow;
115 device->SetWindowBordered = Cocoa_SetWindowBordered;
116 device->SetWindowResizable = Cocoa_SetWindowResizable;
117 device->SetWindowAlwaysOnTop = Cocoa_SetWindowAlwaysOnTop;
118 device->SetWindowFullscreen = Cocoa_SetWindowFullscreen;
119 device->GetWindowICCProfile = Cocoa_GetWindowICCProfile;
120 device->GetDisplayForWindow = Cocoa_GetDisplayForWindow;
121 device->SetWindowMouseRect = Cocoa_SetWindowMouseRect;
122 device->SetWindowMouseGrab = Cocoa_SetWindowMouseGrab;
123 device->SetWindowKeyboardGrab = Cocoa_SetWindowKeyboardGrab;
124 device->DestroyWindow = Cocoa_DestroyWindow;
125 device->SetWindowHitTest = Cocoa_SetWindowHitTest;
126 device->AcceptDragAndDrop = Cocoa_AcceptDragAndDrop;
127 device->UpdateWindowShape = Cocoa_UpdateWindowShape;
128 device->FlashWindow = Cocoa_FlashWindow;
129 device->SetWindowFocusable = Cocoa_SetWindowFocusable;
130 device->SetWindowParent = Cocoa_SetWindowParent;
131 device->SetWindowModal = Cocoa_SetWindowModal;
132 device->SyncWindow = Cocoa_SyncWindow;
133
134#ifdef SDL_VIDEO_OPENGL_CGL
135 device->GL_LoadLibrary = Cocoa_GL_LoadLibrary;
136 device->GL_GetProcAddress = Cocoa_GL_GetProcAddress;
137 device->GL_UnloadLibrary = Cocoa_GL_UnloadLibrary;
138 device->GL_CreateContext = Cocoa_GL_CreateContext;
139 device->GL_MakeCurrent = Cocoa_GL_MakeCurrent;
140 device->GL_SetSwapInterval = Cocoa_GL_SetSwapInterval;
141 device->GL_GetSwapInterval = Cocoa_GL_GetSwapInterval;
142 device->GL_SwapWindow = Cocoa_GL_SwapWindow;
143 device->GL_DestroyContext = Cocoa_GL_DestroyContext;
144 device->GL_GetEGLSurface = NULL;
145#endif
146#ifdef SDL_VIDEO_OPENGL_EGL
147#ifdef SDL_VIDEO_OPENGL_CGL
148 if (SDL_GetHintBoolean(SDL_HINT_VIDEO_FORCE_EGL, false)) {
149#endif
150 device->GL_LoadLibrary = Cocoa_GLES_LoadLibrary;
151 device->GL_GetProcAddress = Cocoa_GLES_GetProcAddress;
152 device->GL_UnloadLibrary = Cocoa_GLES_UnloadLibrary;
153 device->GL_CreateContext = Cocoa_GLES_CreateContext;
154 device->GL_MakeCurrent = Cocoa_GLES_MakeCurrent;
155 device->GL_SetSwapInterval = Cocoa_GLES_SetSwapInterval;
156 device->GL_GetSwapInterval = Cocoa_GLES_GetSwapInterval;
157 device->GL_SwapWindow = Cocoa_GLES_SwapWindow;
158 device->GL_DestroyContext = Cocoa_GLES_DestroyContext;
159 device->GL_GetEGLSurface = Cocoa_GLES_GetEGLSurface;
160#ifdef SDL_VIDEO_OPENGL_CGL
161 }
162#endif
163#endif
164
165#ifdef SDL_VIDEO_VULKAN
166 device->Vulkan_LoadLibrary = Cocoa_Vulkan_LoadLibrary;
167 device->Vulkan_UnloadLibrary = Cocoa_Vulkan_UnloadLibrary;
168 device->Vulkan_GetInstanceExtensions = Cocoa_Vulkan_GetInstanceExtensions;
169 device->Vulkan_CreateSurface = Cocoa_Vulkan_CreateSurface;
170 device->Vulkan_DestroySurface = Cocoa_Vulkan_DestroySurface;
171#endif
172
173#ifdef SDL_VIDEO_METAL
174 device->Metal_CreateView = Cocoa_Metal_CreateView;
175 device->Metal_DestroyView = Cocoa_Metal_DestroyView;
176 device->Metal_GetLayer = Cocoa_Metal_GetLayer;
177#endif
178
179 device->StartTextInput = Cocoa_StartTextInput;
180 device->StopTextInput = Cocoa_StopTextInput;
181 device->UpdateTextInputArea = Cocoa_UpdateTextInputArea;
182
183 device->SetClipboardData = Cocoa_SetClipboardData;
184 device->GetClipboardData = Cocoa_GetClipboardData;
185 device->HasClipboardData = Cocoa_HasClipboardData;
186
187 device->free = Cocoa_DeleteDevice;
188
189 device->device_caps = VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT |
190 VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS;
191 return device;
192 }
193}
194
195VideoBootStrap COCOA_bootstrap = {
196 "cocoa", "SDL Cocoa video driver",
197 Cocoa_CreateDevice,
198 Cocoa_ShowMessageBox,
199 false
200};
201
202static bool Cocoa_VideoInit(SDL_VideoDevice *_this)
203{
204 @autoreleasepool {
205 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
206
207 Cocoa_InitModes(_this);
208 Cocoa_InitKeyboard(_this);
209 if (!Cocoa_InitMouse(_this)) {
210 return false;
211 }
212 if (!Cocoa_InitPen(_this)) {
213 return false;
214 }
215
216 // Assume we have a mouse and keyboard
217 // We could use GCMouse and GCKeyboard if we needed to, as is done in SDL_uikitevents.m
218 SDL_AddKeyboard(SDL_DEFAULT_KEYBOARD_ID, NULL, false);
219 SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL, false);
220
221 data.allow_spaces = SDL_GetHintBoolean(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, true);
222 data.trackpad_is_touch_only = SDL_GetHintBoolean(SDL_HINT_TRACKPAD_IS_TOUCH_ONLY, false);
223 SDL_AddHintCallback(SDL_HINT_VIDEO_MAC_FULLSCREEN_MENU_VISIBILITY, Cocoa_MenuVisibilityCallback, NULL);
224
225 data.swaplock = SDL_CreateMutex();
226 if (!data.swaplock) {
227 return false;
228 }
229
230 return true;
231 }
232}
233
234void Cocoa_VideoQuit(SDL_VideoDevice *_this)
235{
236 @autoreleasepool {
237 SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
238 Cocoa_QuitModes(_this);
239 Cocoa_QuitKeyboard(_this);
240 Cocoa_QuitMouse(_this);
241 Cocoa_QuitPen(_this);
242 SDL_DestroyMutex(data.swaplock);
243 data.swaplock = NULL;
244 }
245}
246
247// This function assumes that it's called from within an autorelease pool
248SDL_SystemTheme Cocoa_GetSystemTheme(void)
249{
250 if (@available(macOS 10.14, *)) {
251 NSAppearance* appearance = [[NSApplication sharedApplication] effectiveAppearance];
252
253 if ([appearance.name containsString: @"Dark"]) {
254 return SDL_SYSTEM_THEME_DARK;
255 }
256 }
257 return SDL_SYSTEM_THEME_LIGHT;
258}
259
260// This function assumes that it's called from within an autorelease pool
261NSImage *Cocoa_CreateImage(SDL_Surface *surface)
262{
263 NSImage *img;
264
265 img = [[NSImage alloc] initWithSize:NSMakeSize(surface->w, surface->h)];
266 if (img == nil) {
267 return nil;
268 }
269
270 SDL_Surface **images = SDL_GetSurfaceImages(surface, NULL);
271 if (!images) {
272 return nil;
273 }
274
275 for (int i = 0; images[i]; ++i) {
276 SDL_Surface *converted = SDL_ConvertSurface(images[i], SDL_PIXELFORMAT_RGBA32);
277 if (!converted) {
278 SDL_free(images);
279 return nil;
280 }
281
282 // Premultiply the alpha channel
283 SDL_PremultiplySurfaceAlpha(converted, false);
284
285 NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
286 pixelsWide:converted->w
287 pixelsHigh:converted->h
288 bitsPerSample:8
289 samplesPerPixel:4
290 hasAlpha:YES
291 isPlanar:NO
292 colorSpaceName:NSDeviceRGBColorSpace
293 bytesPerRow:converted->pitch
294 bitsPerPixel:SDL_BITSPERPIXEL(converted->format)];
295 if (imgrep == nil) {
296 SDL_free(images);
297 SDL_DestroySurface(converted);
298 return nil;
299 }
300
301 // Copy the pixels
302 Uint8 *pixels = [imgrep bitmapData];
303 SDL_memcpy(pixels, converted->pixels, (size_t)converted->h * converted->pitch);
304 SDL_DestroySurface(converted);
305
306 // Add the image representation
307 [img addRepresentation:imgrep];
308 }
309 SDL_free(images);
310
311 return img;
312}
313
314/*
315 * macOS log support.
316 *
317 * This doesn't really have anything to do with the interfaces of the SDL video
318 * subsystem, but we need to stuff this into an Objective-C source code file.
319 *
320 * NOTE: This is copypasted in src/video/uikit/SDL_uikitvideo.m! Be sure both
321 * versions remain identical!
322 */
323
324void SDL_NSLog(const char *prefix, const char *text)
325{
326 @autoreleasepool {
327 NSString *nsText = [NSString stringWithUTF8String:text];
328 if (prefix && *prefix) {
329 NSString *nsPrefix = [NSString stringWithUTF8String:prefix];
330 NSLog(@"%@%@", nsPrefix, nsText);
331 } else {
332 NSLog(@"%@", nsText);
333 }
334 }
335}
336
337#endif // SDL_VIDEO_DRIVER_COCOA
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.h
new file mode 100644
index 0000000..86e634e
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.h
@@ -0,0 +1,52 @@
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
22/*
23 * @author Mark Callow, www.edgewise-consulting.com. Based on Jacob Lifshay's
24 * SDL_x11vulkan.h.
25 */
26
27#include "SDL_internal.h"
28
29#ifndef SDL_cocoavulkan_h_
30#define SDL_cocoavulkan_h_
31
32#include "../SDL_vulkan_internal.h"
33#include "../SDL_sysvideo.h"
34
35#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_COCOA)
36
37extern bool Cocoa_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path);
38extern void Cocoa_Vulkan_UnloadLibrary(SDL_VideoDevice *_this);
39extern char const* const* Cocoa_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, Uint32 *count);
40extern bool Cocoa_Vulkan_CreateSurface(SDL_VideoDevice *_this,
41 SDL_Window *window,
42 VkInstance instance,
43 const struct VkAllocationCallbacks *allocator,
44 VkSurfaceKHR *surface);
45extern void Cocoa_Vulkan_DestroySurface(SDL_VideoDevice *_this,
46 VkInstance instance,
47 VkSurfaceKHR surface,
48 const struct VkAllocationCallbacks *allocator);
49
50#endif
51
52#endif // SDL_cocoavulkan_h_
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.m
new file mode 100644
index 0000000..a440627
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.m
@@ -0,0 +1,304 @@
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
22/*
23 * @author Mark Callow, www.edgewise-consulting.com. Based on Jacob Lifshay's
24 * SDL_x11vulkan.c.
25 */
26#include "SDL_internal.h"
27
28#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_COCOA)
29
30#include "SDL_cocoavideo.h"
31#include "SDL_cocoawindow.h"
32
33#include "SDL_cocoametalview.h"
34#include "SDL_cocoavulkan.h"
35
36#include <dlfcn.h>
37
38const char *defaultPaths[] = {
39 "vulkan.framework/vulkan",
40 "libvulkan.1.dylib",
41 "libvulkan.dylib",
42 "MoltenVK.framework/MoltenVK",
43 "libMoltenVK.dylib"
44};
45
46// Since libSDL is most likely a .dylib, need RTLD_DEFAULT not RTLD_SELF.
47#define DEFAULT_HANDLE RTLD_DEFAULT
48
49bool Cocoa_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path)
50{
51 VkExtensionProperties *extensions = NULL;
52 Uint32 extensionCount = 0;
53 bool hasSurfaceExtension = false;
54 bool hasMetalSurfaceExtension = false;
55 bool hasMacOSSurfaceExtension = false;
56 PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL;
57
58 if (_this->vulkan_config.loader_handle) {
59 return SDL_SetError("Vulkan Portability library is already loaded.");
60 }
61
62 // Load the Vulkan loader library
63 if (!path) {
64 path = SDL_GetHint(SDL_HINT_VULKAN_LIBRARY);
65 }
66
67 if (!path) {
68 // Handle the case where Vulkan Portability is linked statically.
69 vkGetInstanceProcAddr =
70 (PFN_vkGetInstanceProcAddr)dlsym(DEFAULT_HANDLE,
71 "vkGetInstanceProcAddr");
72 }
73
74 if (vkGetInstanceProcAddr) {
75 _this->vulkan_config.loader_handle = DEFAULT_HANDLE;
76 } else {
77 const char **paths;
78 const char *foundPath = NULL;
79 int numPaths;
80 int i;
81
82 if (path) {
83 paths = &path;
84 numPaths = 1;
85 } else {
86 /* Look for framework or .dylib packaged with the application
87 * instead. */
88 paths = defaultPaths;
89 numPaths = SDL_arraysize(defaultPaths);
90 }
91
92 for (i = 0; i < numPaths && _this->vulkan_config.loader_handle == NULL; i++) {
93 foundPath = paths[i];
94 _this->vulkan_config.loader_handle = SDL_LoadObject(foundPath);
95 }
96
97 if (_this->vulkan_config.loader_handle == NULL) {
98 return SDL_SetError("Failed to load Vulkan Portability library");
99 }
100
101 SDL_strlcpy(_this->vulkan_config.loader_path, foundPath,
102 SDL_arraysize(_this->vulkan_config.loader_path));
103 vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)SDL_LoadFunction(
104 _this->vulkan_config.loader_handle, "vkGetInstanceProcAddr");
105 }
106
107 if (!vkGetInstanceProcAddr) {
108 SDL_SetError("Failed to find %s in either executable or %s: %s",
109 "vkGetInstanceProcAddr",
110 _this->vulkan_config.loader_path,
111 (const char *)dlerror());
112 goto fail;
113 }
114
115 _this->vulkan_config.vkGetInstanceProcAddr = (void *)vkGetInstanceProcAddr;
116 _this->vulkan_config.vkEnumerateInstanceExtensionProperties =
117 (void *)((PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr)(
118 VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties");
119 if (!_this->vulkan_config.vkEnumerateInstanceExtensionProperties) {
120 goto fail;
121 }
122 extensions = SDL_Vulkan_CreateInstanceExtensionsList(
123 (PFN_vkEnumerateInstanceExtensionProperties)
124 _this->vulkan_config.vkEnumerateInstanceExtensionProperties,
125 &extensionCount);
126 if (!extensions) {
127 goto fail;
128 }
129 for (Uint32 i = 0; i < extensionCount; i++) {
130 if (SDL_strcmp(VK_KHR_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) {
131 hasSurfaceExtension = true;
132 } else if (SDL_strcmp(VK_EXT_METAL_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) {
133 hasMetalSurfaceExtension = true;
134 } else if (SDL_strcmp(VK_MVK_MACOS_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) {
135 hasMacOSSurfaceExtension = true;
136 }
137 }
138 SDL_free(extensions);
139 if (!hasSurfaceExtension) {
140 SDL_SetError("Installed Vulkan Portability library doesn't implement the " VK_KHR_SURFACE_EXTENSION_NAME " extension");
141 goto fail;
142 } else if (!hasMetalSurfaceExtension && !hasMacOSSurfaceExtension) {
143 SDL_SetError("Installed Vulkan Portability library doesn't implement the " VK_EXT_METAL_SURFACE_EXTENSION_NAME " or " VK_MVK_MACOS_SURFACE_EXTENSION_NAME " extensions");
144 goto fail;
145 }
146 return true;
147
148fail:
149 SDL_UnloadObject(_this->vulkan_config.loader_handle);
150 _this->vulkan_config.loader_handle = NULL;
151 return false;
152}
153
154void Cocoa_Vulkan_UnloadLibrary(SDL_VideoDevice *_this)
155{
156 if (_this->vulkan_config.loader_handle) {
157 if (_this->vulkan_config.loader_handle != DEFAULT_HANDLE) {
158 SDL_UnloadObject(_this->vulkan_config.loader_handle);
159 }
160 _this->vulkan_config.loader_handle = NULL;
161 }
162}
163
164char const* const* Cocoa_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this,
165 Uint32 *count)
166{
167 static const char *const extensionsForCocoa[] = {
168 VK_KHR_SURFACE_EXTENSION_NAME, VK_EXT_METAL_SURFACE_EXTENSION_NAME, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME
169 };
170 if(count) {
171 *count = SDL_arraysize(extensionsForCocoa);
172 }
173 return extensionsForCocoa;
174}
175
176static bool Cocoa_Vulkan_CreateSurfaceViaMetalView(SDL_VideoDevice *_this,
177 SDL_Window *window,
178 VkInstance instance,
179 const struct VkAllocationCallbacks *allocator,
180 VkSurfaceKHR *surface,
181 PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT,
182 PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK)
183{
184 VkResult rc;
185 SDL_MetalView metalview = Cocoa_Metal_CreateView(_this, window);
186 if (metalview == NULL) {
187 return false;
188 }
189
190 if (vkCreateMetalSurfaceEXT) {
191 VkMetalSurfaceCreateInfoEXT createInfo = {};
192 createInfo.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
193 createInfo.pNext = NULL;
194 createInfo.flags = 0;
195 createInfo.pLayer = (__bridge const CAMetalLayer *)
196 Cocoa_Metal_GetLayer(_this, metalview);
197 rc = vkCreateMetalSurfaceEXT(instance, &createInfo, allocator, surface);
198 if (rc != VK_SUCCESS) {
199 Cocoa_Metal_DestroyView(_this, metalview);
200 return SDL_SetError("vkCreateMetalSurfaceEXT failed: %s", SDL_Vulkan_GetResultString(rc));
201 }
202 } else {
203 VkMacOSSurfaceCreateInfoMVK createInfo = {};
204 createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK;
205 createInfo.pNext = NULL;
206 createInfo.flags = 0;
207 createInfo.pView = (const void *)metalview;
208 rc = vkCreateMacOSSurfaceMVK(instance, &createInfo,
209 NULL, surface);
210 if (rc != VK_SUCCESS) {
211 Cocoa_Metal_DestroyView(_this, metalview);
212 return SDL_SetError("vkCreateMacOSSurfaceMVK failed: %s", SDL_Vulkan_GetResultString(rc));
213 }
214 }
215
216 /* Unfortunately there's no SDL_Vulkan_DestroySurface function we can call
217 * Metal_DestroyView from. Right now the metal view's ref count is +2 (one
218 * from returning a new view object in CreateView, and one because it's
219 * a subview of the window.) If we release the view here to make it +1, it
220 * will be destroyed when the window is destroyed.
221 *
222 * TODO: Now that we have SDL_Vulkan_DestroySurface someone with enough
223 * knowledge of Metal can proceed. */
224 CFBridgingRelease(metalview);
225
226 return true; // success!
227}
228
229bool Cocoa_Vulkan_CreateSurface(SDL_VideoDevice *_this,
230 SDL_Window *window,
231 VkInstance instance,
232 const struct VkAllocationCallbacks *allocator,
233 VkSurfaceKHR *surface)
234{
235 PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr =
236 (PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr;
237 PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT =
238 (PFN_vkCreateMetalSurfaceEXT)vkGetInstanceProcAddr(
239 instance,
240 "vkCreateMetalSurfaceEXT");
241 PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK =
242 (PFN_vkCreateMacOSSurfaceMVK)vkGetInstanceProcAddr(
243 instance,
244 "vkCreateMacOSSurfaceMVK");
245 VkResult rc;
246
247 if (!_this->vulkan_config.loader_handle) {
248 return SDL_SetError("Vulkan is not loaded");
249 }
250
251 if (!vkCreateMetalSurfaceEXT && !vkCreateMacOSSurfaceMVK) {
252 return SDL_SetError(VK_EXT_METAL_SURFACE_EXTENSION_NAME " or " VK_MVK_MACOS_SURFACE_EXTENSION_NAME
253 " extensions are not enabled in the Vulkan instance.");
254 }
255
256 if (window->flags & SDL_WINDOW_EXTERNAL) {
257 @autoreleasepool {
258 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
259 if (![data.sdlContentView.layer isKindOfClass:[CAMetalLayer class]]) {
260 [data.sdlContentView setLayer:[CAMetalLayer layer]];
261 }
262
263 if (vkCreateMetalSurfaceEXT) {
264 VkMetalSurfaceCreateInfoEXT createInfo = {};
265 createInfo.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
266 createInfo.pNext = NULL;
267 createInfo.flags = 0;
268 createInfo.pLayer = (CAMetalLayer *)data.sdlContentView.layer;
269 rc = vkCreateMetalSurfaceEXT(instance, &createInfo, allocator, surface);
270 if (rc != VK_SUCCESS) {
271 return SDL_SetError("vkCreateMetalSurfaceEXT failed: %s", SDL_Vulkan_GetResultString(rc));
272 }
273 } else {
274 VkMacOSSurfaceCreateInfoMVK createInfo = {};
275 createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK;
276 createInfo.pNext = NULL;
277 createInfo.flags = 0;
278 createInfo.pView = (__bridge const void *)data.sdlContentView;
279 rc = vkCreateMacOSSurfaceMVK(instance, &createInfo,
280 allocator, surface);
281 if (rc != VK_SUCCESS) {
282 return SDL_SetError("vkCreateMacOSSurfaceMVK failed: %s", SDL_Vulkan_GetResultString(rc));
283 }
284 }
285 }
286 } else {
287 return Cocoa_Vulkan_CreateSurfaceViaMetalView(_this, window, instance, allocator, surface, vkCreateMetalSurfaceEXT, vkCreateMacOSSurfaceMVK);
288 }
289
290 return true;
291}
292
293void Cocoa_Vulkan_DestroySurface(SDL_VideoDevice *_this,
294 VkInstance instance,
295 VkSurfaceKHR surface,
296 const struct VkAllocationCallbacks *allocator)
297{
298 if (_this->vulkan_config.loader_handle) {
299 SDL_Vulkan_DestroySurface_Internal(_this->vulkan_config.vkGetInstanceProcAddr, instance, surface, allocator);
300 // TODO: Add CFBridgingRelease(metalview) here perhaps?
301 }
302}
303
304#endif
diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.h b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.h
new file mode 100644
index 0000000..6df69f4
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.h
@@ -0,0 +1,199 @@
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#ifndef SDL_cocoawindow_h_
24#define SDL_cocoawindow_h_
25
26#import <Cocoa/Cocoa.h>
27
28#ifdef SDL_VIDEO_OPENGL_EGL
29#include "../SDL_egl_c.h"
30#endif
31
32#define SDL_METALVIEW_TAG 255
33
34@class SDL_CocoaWindowData;
35
36typedef enum
37{
38 PENDING_OPERATION_NONE = 0x00,
39 PENDING_OPERATION_ENTER_FULLSCREEN = 0x01,
40 PENDING_OPERATION_LEAVE_FULLSCREEN = 0x02,
41 PENDING_OPERATION_MINIMIZE = 0x04,
42 PENDING_OPERATION_ZOOM = 0x08
43} PendingWindowOperation;
44
45@interface SDL3Cocoa_WindowListener : NSResponder <NSWindowDelegate>
46{
47 /* SDL_CocoaWindowData owns this Listener and has a strong reference to it.
48 * To avoid reference cycles, we could have either a weak or an
49 * unretained ref to the WindowData. */
50 __weak SDL_CocoaWindowData *_data;
51 BOOL observingVisible;
52 BOOL wasCtrlLeft;
53 BOOL wasVisible;
54 BOOL isFullscreenSpace;
55 BOOL inFullscreenTransition;
56 PendingWindowOperation pendingWindowOperation;
57 BOOL isMoving;
58 BOOL isMiniaturizing;
59 NSInteger focusClickPending;
60 float pendingWindowWarpX, pendingWindowWarpY;
61 BOOL isDragAreaRunning;
62 NSTimer *liveResizeTimer;
63}
64
65- (BOOL)isTouchFromTrackpad:(NSEvent *)theEvent;
66- (void)listen:(SDL_CocoaWindowData *)data;
67- (void)pauseVisibleObservation;
68- (void)resumeVisibleObservation;
69- (BOOL)setFullscreenSpace:(BOOL)state;
70- (BOOL)isInFullscreenSpace;
71- (BOOL)isInFullscreenSpaceTransition;
72- (void)addPendingWindowOperation:(PendingWindowOperation)operation;
73- (void)close;
74
75- (BOOL)isMoving;
76- (BOOL)isMovingOrFocusClickPending;
77- (void)setFocusClickPending:(NSInteger)button;
78- (void)clearFocusClickPending:(NSInteger)button;
79- (void)updateIgnoreMouseState:(NSEvent *)theEvent;
80- (void)setPendingMoveX:(float)x Y:(float)y;
81- (void)windowDidFinishMoving;
82- (void)onMovingOrFocusClickPendingStateCleared;
83
84// Window delegate functionality
85- (BOOL)windowShouldClose:(id)sender;
86- (void)windowDidExpose:(NSNotification *)aNotification;
87- (void)windowDidChangeOcclusionState:(NSNotification *)aNotification;
88- (void)windowWillStartLiveResize:(NSNotification *)aNotification;
89- (void)windowDidEndLiveResize:(NSNotification *)aNotification;
90- (void)windowDidMove:(NSNotification *)aNotification;
91- (void)windowDidResize:(NSNotification *)aNotification;
92- (void)windowDidMiniaturize:(NSNotification *)aNotification;
93- (void)windowDidDeminiaturize:(NSNotification *)aNotification;
94- (void)windowDidBecomeKey:(NSNotification *)aNotification;
95- (void)windowDidResignKey:(NSNotification *)aNotification;
96- (void)windowDidChangeBackingProperties:(NSNotification *)aNotification;
97- (void)windowDidChangeScreenProfile:(NSNotification *)aNotification;
98- (void)windowDidChangeScreen:(NSNotification *)aNotification;
99- (void)windowWillEnterFullScreen:(NSNotification *)aNotification;
100- (void)windowDidEnterFullScreen:(NSNotification *)aNotification;
101- (void)windowWillExitFullScreen:(NSNotification *)aNotification;
102- (void)windowDidExitFullScreen:(NSNotification *)aNotification;
103- (NSApplicationPresentationOptions)window:(NSWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions;
104
105// See if event is in a drag area, toggle on window dragging.
106- (void)updateHitTest;
107- (BOOL)processHitTest:(NSEvent *)theEvent;
108
109// Window event handling
110- (void)mouseDown:(NSEvent *)theEvent;
111- (void)rightMouseDown:(NSEvent *)theEvent;
112- (void)otherMouseDown:(NSEvent *)theEvent;
113- (void)mouseUp:(NSEvent *)theEvent;
114- (void)rightMouseUp:(NSEvent *)theEvent;
115- (void)otherMouseUp:(NSEvent *)theEvent;
116- (void)mouseMoved:(NSEvent *)theEvent;
117- (void)mouseDragged:(NSEvent *)theEvent;
118- (void)rightMouseDragged:(NSEvent *)theEvent;
119- (void)otherMouseDragged:(NSEvent *)theEvent;
120- (void)scrollWheel:(NSEvent *)theEvent;
121- (void)touchesBeganWithEvent:(NSEvent *)theEvent;
122- (void)touchesMovedWithEvent:(NSEvent *)theEvent;
123- (void)touchesEndedWithEvent:(NSEvent *)theEvent;
124- (void)touchesCancelledWithEvent:(NSEvent *)theEvent;
125
126// Touch event handling
127- (void)handleTouches:(NSTouchPhase)phase withEvent:(NSEvent *)theEvent;
128
129// Tablet event handling (but these also come through on mouse events sometimes!)
130- (void)tabletProximity:(NSEvent *)theEvent;
131- (void)tabletPoint:(NSEvent *)theEvent;
132
133@end
134/* *INDENT-ON* */
135
136@class SDL3OpenGLContext;
137@class SDL_CocoaVideoData;
138
139@interface SDL_CocoaWindowData : NSObject
140@property(nonatomic) SDL_Window *window;
141@property(nonatomic) NSWindow *nswindow;
142@property(nonatomic) NSView *sdlContentView;
143@property(nonatomic) NSMutableArray *nscontexts;
144@property(nonatomic) BOOL in_blocking_transition;
145@property(nonatomic) BOOL fullscreen_space_requested;
146@property(nonatomic) BOOL was_zoomed;
147@property(nonatomic) NSInteger window_number;
148@property(nonatomic) NSInteger flash_request;
149@property(nonatomic) SDL_Window *keyboard_focus;
150@property(nonatomic) SDL3Cocoa_WindowListener *listener;
151@property(nonatomic) NSModalSession modal_session;
152@property(nonatomic) SDL_CocoaVideoData *videodata;
153@property(nonatomic) bool pending_size;
154@property(nonatomic) bool pending_position;
155@property(nonatomic) bool border_toggled;
156
157#ifdef SDL_VIDEO_OPENGL_EGL
158@property(nonatomic) EGLSurface egl_surface;
159#endif
160@end
161
162extern bool b_inModeTransition;
163
164extern bool Cocoa_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props);
165extern void Cocoa_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window);
166extern bool Cocoa_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon);
167extern bool Cocoa_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window);
168extern void Cocoa_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window);
169extern void Cocoa_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window);
170extern void Cocoa_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window);
171extern void Cocoa_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window);
172extern void Cocoa_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h);
173extern bool Cocoa_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity);
174extern void Cocoa_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window);
175extern void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window);
176extern void Cocoa_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window);
177extern void Cocoa_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window);
178extern void Cocoa_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window);
179extern void Cocoa_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window);
180extern void Cocoa_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered);
181extern void Cocoa_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable);
182extern void Cocoa_SetWindowAlwaysOnTop(SDL_VideoDevice *_this, SDL_Window *window, bool on_top);
183extern SDL_FullscreenResult Cocoa_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen);
184extern void *Cocoa_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size);
185extern SDL_DisplayID Cocoa_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *window);
186extern bool Cocoa_SetWindowMouseRect(SDL_VideoDevice *_this, SDL_Window *window);
187extern bool Cocoa_SetWindowMouseGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed);
188extern void Cocoa_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window);
189extern bool Cocoa_SetWindowHitTest(SDL_Window *window, bool enabled);
190extern void Cocoa_AcceptDragAndDrop(SDL_Window *window, bool accept);
191extern bool Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation);
192extern bool Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable);
193extern bool Cocoa_SetWindowModal(SDL_VideoDevice *_this, SDL_Window *window, bool modal);
194extern bool Cocoa_SetWindowParent(SDL_VideoDevice *_this, SDL_Window *window, SDL_Window *parent);
195extern bool Cocoa_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window);
196
197extern void Cocoa_MenuVisibilityCallback(void *userdata, const char *name, const char *oldValue, const char *newValue);
198
199#endif // SDL_cocoawindow_h_
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