From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- .../SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.h | 34 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.m | 262 ++ .../SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.h | 33 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.m | 680 ++++ .../SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.h | 36 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m | 604 ++++ .../src/video/cocoa/SDL_cocoamessagebox.h | 27 + .../src/video/cocoa/SDL_cocoamessagebox.m | 145 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.h | 66 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.m | 182 ++ contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.h | 45 + contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.m | 716 +++++ contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.h | 51 + contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.m | 591 ++++ .../SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.h | 88 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.m | 559 ++++ .../SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.h | 48 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.m | 156 + contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.h | 32 + contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.m | 178 ++ contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.h | 28 + contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.m | 54 + contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.h | 71 + contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.m | 337 ++ .../SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.h | 52 + .../SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.m | 304 ++ .../SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.h | 199 ++ .../SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m | 3277 ++++++++++++++++++++ 28 files changed, 8855 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaclipboard.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaevents.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamessagebox.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoametalview.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamodes.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoamouse.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengl.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoaopengles.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoapen.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoashape.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavideo.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoavulkan.m create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.h create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoawindow.m (limited to 'contrib/SDL-3.2.8/src/video/cocoa') 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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoaclipboard_h_ +#define SDL_cocoaclipboard_h_ + +// Forward declaration +@class SDL_CocoaVideoData; + +extern void Cocoa_CheckClipboardUpdate(SDL_CocoaVideoData *data); +extern bool Cocoa_SetClipboardData(SDL_VideoDevice *_this); +extern void *Cocoa_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *size); +extern bool Cocoa_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoavideo.h" +#include "../../events/SDL_events_c.h" +#include "../../events/SDL_clipboardevents_c.h" + +#include + +@interface Cocoa_PasteboardDataProvider : NSObject +{ + SDL_ClipboardDataCallback m_callback; + void *m_userdata; +} +@end + +@implementation Cocoa_PasteboardDataProvider + +- (nullable instancetype)initWith:(SDL_ClipboardDataCallback)callback + userData:(void *)userdata +{ + self = [super init]; + if (!self) { + return self; + } + m_callback = callback; + m_userdata = userdata; + return self; +} + +- (void)pasteboard:(NSPasteboard *)pasteboard + item:(NSPasteboardItem *)item +provideDataForType:(NSPasteboardType)type +{ + @autoreleasepool { + size_t size = 0; + CFStringRef mimeType; + const void *callbackData; + NSData *data; + mimeType = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)type, kUTTagClassMIMEType); + callbackData = m_callback(m_userdata, [(__bridge NSString *)mimeType UTF8String], &size); + CFRelease(mimeType); + if (callbackData == NULL || size == 0) { + return; + } + data = [NSData dataWithBytes: callbackData length: size]; + [item setData: data forType: type]; + } +} + +@end + +static char **GetMimeTypes(int *pnformats) +{ + char **new_mime_types = NULL; + + *pnformats = 0; + + int nformats = 0; + int formatsSz = 0; + NSArray *items = [[NSPasteboard generalPasteboard] pasteboardItems]; + NSUInteger nitems = [items count]; + if (nitems > 0) { + for (NSPasteboardItem *item in items) { + NSArray *types = [item types]; + for (NSString *type in types) { + if (@available(macOS 11.0, *)) { + UTType *uttype = [UTType typeWithIdentifier:type]; + NSString *mime_type = [uttype preferredMIMEType]; + if (mime_type) { + NSUInteger len = [mime_type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; + formatsSz += len; + ++nformats; + } + } + NSUInteger len = [type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; + formatsSz += len; + ++nformats; + } + } + + new_mime_types = SDL_AllocateTemporaryMemory((nformats + 1) * sizeof(char *) + formatsSz); + if (new_mime_types) { + int i = 0; + char *strPtr = (char *)(new_mime_types + nformats + 1); + for (NSPasteboardItem *item in items) { + NSArray *types = [item types]; + for (NSString *type in types) { + if (@available(macOS 11.0, *)) { + UTType *uttype = [UTType typeWithIdentifier:type]; + NSString *mime_type = [uttype preferredMIMEType]; + if (mime_type) { + NSUInteger len = [mime_type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; + SDL_memcpy(strPtr, [mime_type UTF8String], len); + new_mime_types[i++] = strPtr; + strPtr += len; + } + } + NSUInteger len = [type lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1; + SDL_memcpy(strPtr, [type UTF8String], len); + new_mime_types[i++] = strPtr; + strPtr += len; + } + } + + new_mime_types[nformats] = NULL; + *pnformats = nformats; + } + } + return new_mime_types; +} + + +void Cocoa_CheckClipboardUpdate(SDL_CocoaVideoData *data) +{ + @autoreleasepool { + NSPasteboard *pasteboard; + NSInteger count; + + pasteboard = [NSPasteboard generalPasteboard]; + count = [pasteboard changeCount]; + if (count != data.clipboard_count) { + if (count) { + int nformats = 0; + char **new_mime_types = GetMimeTypes(&nformats); + if (new_mime_types) { + SDL_SendClipboardUpdate(false, new_mime_types, nformats); + } + } + data.clipboard_count = count; + } + } +} + +bool Cocoa_SetClipboardData(SDL_VideoDevice *_this) +{ + @autoreleasepool { + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + NSPasteboardItem *newItem = [NSPasteboardItem new]; + NSMutableArray *utiTypes = [NSMutableArray new]; + Cocoa_PasteboardDataProvider *provider = [[Cocoa_PasteboardDataProvider alloc] initWith: _this->clipboard_callback userData: _this->clipboard_userdata]; + BOOL itemResult = FALSE; + BOOL writeResult = FALSE; + + if (_this->clipboard_callback) { + for (int i = 0; i < _this->num_clipboard_mime_types; i++) { + CFStringRef mimeType = CFStringCreateWithCString(NULL, _this->clipboard_mime_types[i], kCFStringEncodingUTF8); + CFStringRef utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); + CFRelease(mimeType); + + [utiTypes addObject: (__bridge NSString *)utiType]; + CFRelease(utiType); + } + itemResult = [newItem setDataProvider: provider forTypes: utiTypes]; + if (itemResult == FALSE) { + return SDL_SetError("Unable to set clipboard item data"); + } + + [pasteboard clearContents]; + writeResult = [pasteboard writeObjects: @[newItem]]; + if (writeResult == FALSE) { + return SDL_SetError("Unable to set clipboard data"); + } + } else { + [pasteboard clearContents]; + } + data.clipboard_count = [pasteboard changeCount]; + } + return true; +} + +static bool IsMimeType(const char *tag) +{ + if (SDL_strchr(tag, '/')) { + // MIME types have slashes + return true; + } else if (SDL_strchr(tag, '.')) { + // UTI identifiers have periods + return false; + } else { + // Not sure, but it's not a UTI identifier + return true; + } +} + +static CFStringRef GetUTIType(const char *tag) +{ + CFStringRef utiType; + if (IsMimeType(tag)) { + CFStringRef mimeType = CFStringCreateWithCString(NULL, tag, kCFStringEncodingUTF8); + utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType, NULL); + CFRelease(mimeType); + } else { + utiType = CFStringCreateWithCString(NULL, tag, kCFStringEncodingUTF8); + } + return utiType; +} + +void *Cocoa_GetClipboardData(SDL_VideoDevice *_this, const char *mime_type, size_t *size) +{ + @autoreleasepool { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + void *data = NULL; + *size = 0; + for (NSPasteboardItem *item in [pasteboard pasteboardItems]) { + NSData *itemData; + CFStringRef utiType = GetUTIType(mime_type); + itemData = [item dataForType: (__bridge NSString *)utiType]; + CFRelease(utiType); + if (itemData != nil) { + NSUInteger length = [itemData length]; + *size = (size_t)length; + data = SDL_malloc(*size + sizeof(Uint32)); + if (data) { + [itemData getBytes: data length: length]; + SDL_memset((Uint8 *)data + length, 0, sizeof(Uint32)); + } + break; + } + } + return data; + } +} + +bool Cocoa_HasClipboardData(SDL_VideoDevice *_this, const char *mime_type) +{ + bool result = false; + @autoreleasepool { + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + CFStringRef utiType = GetUTIType(mime_type); + if ([pasteboard canReadItemWithDataConformingToTypes: @[(__bridge NSString *)utiType]]) { + result = true; + } + CFRelease(utiType); + } + return result; + +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoaevents_h_ +#define SDL_cocoaevents_h_ + +extern void Cocoa_RegisterApp(void); +extern Uint64 Cocoa_GetEventTimestamp(NSTimeInterval nsTimestamp); +extern void Cocoa_PumpEvents(SDL_VideoDevice *_this); +extern int Cocoa_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS); +extern void Cocoa_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_SuspendScreenSaver(SDL_VideoDevice *_this); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoavideo.h" +#include "../../events/SDL_events_c.h" + +static SDL_Window *FindSDLWindowForNSWindow(NSWindow *win) +{ + SDL_Window *sdlwindow = NULL; + SDL_VideoDevice *device = SDL_GetVideoDevice(); + if (device && device->windows) { + for (sdlwindow = device->windows; sdlwindow; sdlwindow = sdlwindow->next) { + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)sdlwindow->internal).nswindow; + if (win == nswindow) { + return sdlwindow; + } + } + } + + return sdlwindow; +} + +@interface SDL3Application : NSApplication + +- (void)terminate:(id)sender; +- (void)sendEvent:(NSEvent *)theEvent; + ++ (void)registerUserDefaults; + +@end + +@implementation SDL3Application + +// Override terminate to handle Quit and System Shutdown smoothly. +- (void)terminate:(id)sender +{ + SDL_SendQuit(); +} + +static bool s_bShouldHandleEventsInSDLApplication = false; + +static void Cocoa_DispatchEvent(NSEvent *theEvent) +{ + SDL_VideoDevice *_this = SDL_GetVideoDevice(); + + switch ([theEvent type]) { + case NSEventTypeLeftMouseDown: + case NSEventTypeOtherMouseDown: + case NSEventTypeRightMouseDown: + case NSEventTypeLeftMouseUp: + case NSEventTypeOtherMouseUp: + case NSEventTypeRightMouseUp: + case NSEventTypeLeftMouseDragged: + case NSEventTypeRightMouseDragged: + case NSEventTypeOtherMouseDragged: // usually middle mouse dragged + case NSEventTypeMouseMoved: + case NSEventTypeScrollWheel: + case NSEventTypeMouseEntered: + case NSEventTypeMouseExited: + Cocoa_HandleMouseEvent(_this, theEvent); + break; + case NSEventTypeKeyDown: + case NSEventTypeKeyUp: + case NSEventTypeFlagsChanged: + Cocoa_HandleKeyEvent(_this, theEvent); + break; + default: + break; + } +} + +// Dispatch events here so that we can handle events caught by +// nextEventMatchingMask in SDL, as well as events caught by other +// processes (such as CEF) that are passed down to NSApp. +- (void)sendEvent:(NSEvent *)theEvent +{ + if (s_bShouldHandleEventsInSDLApplication) { + Cocoa_DispatchEvent(theEvent); + } + + [super sendEvent:theEvent]; +} + ++ (void)registerUserDefaults +{ + BOOL momentumScrollSupported = (BOOL)SDL_GetHintBoolean(SDL_HINT_MAC_SCROLL_MOMENTUM, false); + + NSDictionary *appDefaults = [[NSDictionary alloc] initWithObjectsAndKeys: + [NSNumber numberWithBool:momentumScrollSupported], @"AppleMomentumScrollSupported", + [NSNumber numberWithBool:YES], @"ApplePressAndHoldEnabled", + [NSNumber numberWithBool:YES], @"ApplePersistenceIgnoreState", + nil]; + [[NSUserDefaults standardUserDefaults] registerDefaults:appDefaults]; +} + +@end // SDL3Application + +// setAppleMenu disappeared from the headers in 10.4 +@interface NSApplication (NSAppleMenu) +- (void)setAppleMenu:(NSMenu *)menu; +@end + +@interface SDL3AppDelegate : NSObject +{ + @public + BOOL seenFirstActivate; +} + +- (id)init; +- (void)localeDidChange:(NSNotification *)notification; +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context; +- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app; +- (IBAction)menu:(id)sender; +@end + +@implementation SDL3AppDelegate : NSObject +- (id)init +{ + self = [super init]; + if (self) { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + bool registerActivationHandlers = SDL_GetHintBoolean("SDL_MAC_REGISTER_ACTIVATION_HANDLERS", true); + + seenFirstActivate = NO; + + if (registerActivationHandlers) { + [center addObserver:self + selector:@selector(windowWillClose:) + name:NSWindowWillCloseNotification + object:nil]; + + [center addObserver:self + selector:@selector(focusSomeWindow:) + name:NSApplicationDidBecomeActiveNotification + object:nil]; + + [center addObserver:self + selector:@selector(screenParametersChanged:) + name:NSApplicationDidChangeScreenParametersNotification + object:nil]; + } + + [center addObserver:self + selector:@selector(localeDidChange:) + name:NSCurrentLocaleDidChangeNotification + object:nil]; + + [NSApp addObserver:self + forKeyPath:@"effectiveAppearance" + options:NSKeyValueObservingOptionInitial + context:nil]; + } + + return self; +} + +- (void)dealloc +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + [center removeObserver:self name:NSWindowWillCloseNotification object:nil]; + [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil]; + [center removeObserver:self name:NSApplicationDidChangeScreenParametersNotification object:nil]; + [center removeObserver:self name:NSCurrentLocaleDidChangeNotification object:nil]; + [NSApp removeObserver:self forKeyPath:@"effectiveAppearance"]; + + // Remove our URL event handler only if we set it + if ([NSApp delegate] == self) { + [[NSAppleEventManager sharedAppleEventManager] + removeEventHandlerForEventClass:kInternetEventClass + andEventID:kAEGetURL]; + } +} + +- (void)windowWillClose:(NSNotification *)notification +{ + NSWindow *win = (NSWindow *)[notification object]; + + if (![win isKeyWindow]) { + return; + } + + // Don't do anything if this was not an SDL window that was closed + if (FindSDLWindowForNSWindow(win) == NULL) { + return; + } + + /* HACK: Make the next window in the z-order key when the key window is + * closed. The custom event loop and/or windowing code we have seems to + * prevent the normal behavior: https://bugzilla.libsdl.org/show_bug.cgi?id=1825 + */ + + /* +[NSApp orderedWindows] never includes the 'About' window, but we still + * want to try its list first since the behavior in other apps is to only + * make the 'About' window key if no other windows are on-screen. + */ + for (NSWindow *window in [NSApp orderedWindows]) { + if (window != win && [window canBecomeKeyWindow]) { + if (![window isOnActiveSpace]) { + continue; + } + [window makeKeyAndOrderFront:self]; + return; + } + } + + /* If a window wasn't found above, iterate through all visible windows in + * the active Space in z-order (including the 'About' window, if it's shown) + * and make the first one key. + */ + for (NSNumber *num in [NSWindow windowNumbersWithOptions:0]) { + NSWindow *window = [NSApp windowWithWindowNumber:[num integerValue]]; + if (window && window != win && [window canBecomeKeyWindow]) { + [window makeKeyAndOrderFront:self]; + return; + } + } +} + +- (void)focusSomeWindow:(NSNotification *)aNotification +{ + SDL_VideoDevice *device; + /* HACK: Ignore the first call. The application gets a + * applicationDidBecomeActive: a little bit after the first window is + * created, and if we don't ignore it, a window that has been created with + * SDL_WINDOW_MINIMIZED will ~immediately be restored. + */ + if (!seenFirstActivate) { + seenFirstActivate = YES; + return; + } + + /* Don't do anything if the application already has a key window + * that is not an SDL window. + */ + if ([NSApp keyWindow] && FindSDLWindowForNSWindow([NSApp keyWindow]) == NULL) { + return; + } + + device = SDL_GetVideoDevice(); + if (device && device->windows) { + SDL_Window *window = device->windows; + int i; + for (i = 0; i < device->num_displays; ++i) { + SDL_Window *fullscreen_window = device->displays[i]->fullscreen_window; + if (fullscreen_window) { + if (fullscreen_window->flags & SDL_WINDOW_MINIMIZED) { + SDL_RestoreWindow(fullscreen_window); + } + return; + } + } + + if (window->flags & SDL_WINDOW_MINIMIZED) { + SDL_RestoreWindow(window); + } else { + SDL_RaiseWindow(window); + } + } +} + +- (void)screenParametersChanged:(NSNotification *)aNotification +{ + SDL_VideoDevice *device = SDL_GetVideoDevice(); + if (device) { + Cocoa_UpdateDisplays(device); + } +} + +- (void)localeDidChange:(NSNotification *)notification +{ + SDL_SendLocaleChangedEvent(); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + SDL_SetSystemTheme(Cocoa_GetSystemTheme()); +} + +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename +{ + return (BOOL)SDL_SendDropFile(NULL, NULL, [filename UTF8String]) && SDL_SendDropComplete(NULL); +} + +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + if (!SDL_GetHintBoolean("SDL_MAC_REGISTER_ACTIVATION_HANDLERS", true)) + return; + + /* The menu bar of SDL apps which don't have the typical .app bundle + * structure fails to work the first time a window is created (until it's + * de-focused and re-focused), if this call is in Cocoa_RegisterApp instead + * of here. https://bugzilla.libsdl.org/show_bug.cgi?id=3051 + */ + if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, false)) { + // Get more aggressive for Catalina: activate the Dock first so we definitely reset all activation state. + for (NSRunningApplication *i in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) { + [i activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + break; + } + SDL_Delay(300); // !!! FIXME: this isn't right. + [NSApp activateIgnoringOtherApps:YES]; + } + + /* If we call this before NSApp activation, macOS might print a complaint + * about ApplePersistenceIgnoreState. */ + [SDL3Application registerUserDefaults]; +} + +- (void)handleURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent +{ + NSString *path = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + SDL_SendDropFile(NULL, NULL, [path UTF8String]); + SDL_SendDropComplete(NULL); +} + +- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app +{ + // This just tells Cocoa that we didn't do any custom save state magic for the app, + // so the system is safe to use NSSecureCoding internally, instead of using unencrypted + // save states for backwards compatibility. If we don't return YES here, we'll get a + // warning on the console at startup: + // + // ``` + // WARNING: Secure coding is not enabled for restorable state! Enable secure coding by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState: and returning YES. + // ``` + // + // More-detailed explanation: + // https://stackoverflow.com/questions/77283578/sonoma-and-nsapplicationdelegate-applicationsupportssecurerestorablestate/77320845#77320845 + return YES; +} + +- (IBAction)menu:(id)sender +{ + SDL_TrayEntry *entry = [[sender representedObject] pointerValue]; + + SDL_ClickTrayEntry(entry); +} + +@end + +static SDL3AppDelegate *appDelegate = nil; + +static NSString *GetApplicationName(void) +{ + NSString *appName = nil; + + const char *metaname = SDL_GetStringProperty(SDL_GetGlobalProperties(), SDL_PROP_APP_METADATA_NAME_STRING, NULL); + if (metaname && *metaname) { + appName = [NSString stringWithUTF8String:metaname]; + } + + // Determine the application name + if (!appName) { + appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + if (!appName) { + appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; + } + } + + if (![appName length]) { + appName = [[NSProcessInfo processInfo] processName]; + } + + return appName; +} + +static bool LoadMainMenuNibIfAvailable(void) +{ + NSDictionary *infoDict; + NSString *mainNibFileName; + bool success = false; + + infoDict = [[NSBundle mainBundle] infoDictionary]; + if (infoDict) { + mainNibFileName = [infoDict valueForKey:@"NSMainNibFile"]; + + if (mainNibFileName) { + success = [[NSBundle mainBundle] loadNibNamed:mainNibFileName owner:[NSApplication sharedApplication] topLevelObjects:nil]; + } + } + + return success; +} + +static void CreateApplicationMenus(void) +{ + NSString *appName; + NSString *title; + NSMenu *appleMenu; + NSMenu *serviceMenu; + NSMenu *windowMenu; + NSMenuItem *menuItem; + NSMenu *mainMenu; + + if (NSApp == nil) { + return; + } + + mainMenu = [[NSMenu alloc] init]; + + // Create the main menu bar + [NSApp setMainMenu:mainMenu]; + + // Create the application menu + appName = GetApplicationName(); + appleMenu = [[NSMenu alloc] initWithTitle:@""]; + + // Add menu items + title = [@"About " stringByAppendingString:appName]; + + // !!! FIXME: Menu items can't take parameters, just a basic selector, so this should instead call a selector + // !!! FIXME: that itself calls -[NSApplication orderFrontStandardAboutPanelWithOptions:optionsDictionary], + // !!! FIXME: filling in that NSDictionary with SDL_GetAppMetadataProperty() + [appleMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; + + [appleMenu addItem:[NSMenuItem separatorItem]]; + + [appleMenu addItemWithTitle:@"Preferences…" action:nil keyEquivalent:@","]; + + [appleMenu addItem:[NSMenuItem separatorItem]]; + + serviceMenu = [[NSMenu alloc] initWithTitle:@""]; + menuItem = [appleMenu addItemWithTitle:@"Services" action:nil keyEquivalent:@""]; + [menuItem setSubmenu:serviceMenu]; + + [NSApp setServicesMenu:serviceMenu]; + + [appleMenu addItem:[NSMenuItem separatorItem]]; + + title = [@"Hide " stringByAppendingString:appName]; + [appleMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"]; + + menuItem = [appleMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; + [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption | NSEventModifierFlagCommand)]; + + [appleMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; + + [appleMenu addItem:[NSMenuItem separatorItem]]; + + title = [@"Quit " stringByAppendingString:appName]; + [appleMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; + + // Put menu into the menubar + menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; + [menuItem setSubmenu:appleMenu]; + [[NSApp mainMenu] addItem:menuItem]; + + // Tell the application object that this is now the application menu + [NSApp setAppleMenu:appleMenu]; + + // Create the window menu + windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; + + // Add menu items + [windowMenu addItemWithTitle:@"Close" action:@selector(performClose:) keyEquivalent:@"w"]; + + [windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; + + [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; + + // Add the fullscreen toggle menu option. + /* Cocoa should update the title to Enter or Exit Full Screen automatically. + * But if not, then just fallback to Toggle Full Screen. + */ + menuItem = [[NSMenuItem alloc] initWithTitle:@"Toggle Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"f"]; + [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand]; + [windowMenu addItem:menuItem]; + + // Put menu into the menubar + menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; + [menuItem setSubmenu:windowMenu]; + [[NSApp mainMenu] addItem:menuItem]; + + // Tell the application object that this is now the window menu + [NSApp setWindowsMenu:windowMenu]; +} + +void Cocoa_RegisterApp(void) +{ + @autoreleasepool { + // This can get called more than once! Be careful what you initialize! + + if (NSApp == nil) { + [SDL3Application sharedApplication]; + SDL_assert(NSApp != nil); + + s_bShouldHandleEventsInSDLApplication = true; + + if (!SDL_GetHintBoolean(SDL_HINT_MAC_BACKGROUND_APP, false)) { + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + } + + /* If there aren't already menus in place, look to see if there's + * a nib we should use. If not, then manually create the basic + * menus we meed. + */ + if ([NSApp mainMenu] == nil) { + bool nibLoaded; + + nibLoaded = LoadMainMenuNibIfAvailable(); + if (!nibLoaded) { + CreateApplicationMenus(); + } + } + [NSApp finishLaunching]; + if ([NSApp delegate]) { + /* The SDL app delegate calls this in didFinishLaunching if it's + * attached to the NSApp, otherwise we need to call it manually. + */ + [SDL3Application registerUserDefaults]; + } + } + if (NSApp && !appDelegate) { + appDelegate = [[SDL3AppDelegate alloc] init]; + + /* If someone else has an app delegate, it means we can't turn a + * termination into SDL_Quit, and we can't handle application:openFile: + */ + if (![NSApp delegate]) { + /* Only register the URL event handler if we are being set as the + * app delegate to avoid replacing any existing event handler. + */ + [[NSAppleEventManager sharedAppleEventManager] + setEventHandler:appDelegate + andSelector:@selector(handleURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; + + [(NSApplication *)NSApp setDelegate:appDelegate]; + } else { + appDelegate->seenFirstActivate = YES; + } + } + } +} + +Uint64 Cocoa_GetEventTimestamp(NSTimeInterval nsTimestamp) +{ + static Uint64 timestamp_offset; + Uint64 timestamp = (Uint64)(nsTimestamp * SDL_NS_PER_SECOND); + Uint64 now = SDL_GetTicksNS(); + + if (!timestamp_offset) { + timestamp_offset = (now - timestamp); + } + timestamp += timestamp_offset; + + if (timestamp > now) { + timestamp_offset -= (timestamp - now); + timestamp = now; + } + return timestamp; +} + +int Cocoa_PumpEventsUntilDate(SDL_VideoDevice *_this, NSDate *expiration, bool accumulate) +{ + // Run any existing modal sessions. + for (SDL_Window *w = _this->windows; w; w = w->next) { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)w->internal; + if (data.modal_session) { + [NSApp runModalSession:data.modal_session]; + } + } + + for (;;) { + NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:YES]; + if (event == nil) { + return 0; + } + + if (!s_bShouldHandleEventsInSDLApplication) { + Cocoa_DispatchEvent(event); + } + + // Pass events down to SDL3Application to be handled in sendEvent: + [NSApp sendEvent:event]; + if (!accumulate) { + break; + } + } + return 1; +} + +int Cocoa_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS) +{ + @autoreleasepool { + if (timeoutNS > 0) { + NSDate *limitDate = [NSDate dateWithTimeIntervalSinceNow:(double)timeoutNS / SDL_NS_PER_SECOND]; + return Cocoa_PumpEventsUntilDate(_this, limitDate, false); + } else if (timeoutNS == 0) { + return Cocoa_PumpEventsUntilDate(_this, [NSDate distantPast], false); + } else { + while (Cocoa_PumpEventsUntilDate(_this, [NSDate distantFuture], false) == 0) { + } + } + return 1; + } +} + +void Cocoa_PumpEvents(SDL_VideoDevice *_this) +{ + @autoreleasepool { + Cocoa_PumpEventsUntilDate(_this, [NSDate distantPast], true); + } +} + +void Cocoa_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0.0 + windowNumber:((__bridge SDL_CocoaWindowData *)window->internal).window_number + context:nil + subtype:0 + data1:0 + data2:0]; + + [NSApp postEvent:event atStart:YES]; + } +} + +bool Cocoa_SuspendScreenSaver(SDL_VideoDevice *_this) +{ + @autoreleasepool { + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + + if (data.screensaver_assertion) { + IOPMAssertionRelease(data.screensaver_assertion); + data.screensaver_assertion = kIOPMNullAssertionID; + } + + if (_this->suspend_screensaver) { + /* FIXME: this should ideally describe the real reason why the game + * called SDL_DisableScreenSaver. Note that the name is only meant to be + * seen by macOS power users. there's an additional optional human-readable + * (localized) reason parameter which we don't set. + */ + IOPMAssertionID assertion = kIOPMNullAssertionID; + NSString *name = [GetApplicationName() stringByAppendingString:@" using SDL_DisableScreenSaver"]; + IOPMAssertionCreateWithDescription(kIOPMAssertPreventUserIdleDisplaySleep, + (__bridge CFStringRef)name, + NULL, NULL, NULL, 0, NULL, + &assertion); + data.screensaver_assertion = assertion; + } + } + return true; +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoakeyboard_h_ +#define SDL_cocoakeyboard_h_ + +extern void Cocoa_InitKeyboard(SDL_VideoDevice *_this); +extern void Cocoa_HandleKeyEvent(SDL_VideoDevice *_this, NSEvent *event); +extern void Cocoa_QuitKeyboard(SDL_VideoDevice *_this); + +extern bool Cocoa_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props); +extern bool Cocoa_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window); + +extern bool Cocoa_SetWindowKeyboardGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoavideo.h" + +#include "../../events/SDL_events_c.h" +#include "../../events/SDL_keyboard_c.h" +#include "../../events/scancodes_darwin.h" + +#include + +#if 0 +#define DEBUG_IME NSLog +#else +#define DEBUG_IME(...) +#endif + +@interface SDL3TranslatorResponder : NSView +{ + NSString *_markedText; + NSRange _markedRange; + NSRange _selectedRange; + SDL_Rect _inputRect; + int _pendingRawCode; + SDL_Scancode _pendingScancode; + Uint64 _pendingTimestamp; +} +- (void)doCommandBySelector:(SEL)myselector; +- (void)setInputRect:(const SDL_Rect *)rect; +- (void)setPendingKey:(int)rawcode scancode:(SDL_Scancode)scancode timestamp:(Uint64)timestamp; +- (void)sendPendingKey; +- (void)clearPendingKey; +@end + +@implementation SDL3TranslatorResponder + +- (void)setInputRect:(const SDL_Rect *)rect +{ + SDL_copyp(&_inputRect, rect); +} + +- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange +{ + const char *str; + + DEBUG_IME(@"insertText: %@ replacementRange: (%d, %d)", aString, + (int)replacementRange.location, (int)replacementRange.length); + + /* Could be NSString or NSAttributedString, so we have + * to test and convert it before return as SDL event */ + if ([aString isKindOfClass:[NSAttributedString class]]) { + str = [[aString string] UTF8String]; + } else { + str = [aString UTF8String]; + } + + // We're likely sending the composed text, so we reset the IME status. + if ([self hasMarkedText]) { + [self unmarkText]; + } + + // Deliver the raw key event that generated this text + [self sendPendingKey]; + + if ((int)replacementRange.location != -1) { + // We're replacing the last character + SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, true); + SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, false); + } + + SDL_SendKeyboardText(str); +} + +- (void)doCommandBySelector:(SEL)myselector +{ + /* No need to do anything since we are not using Cocoa + selectors to handle special keys, instead we use SDL + key events to do the same job. + */ +} + +- (BOOL)hasMarkedText +{ + return _markedText != nil; +} + +- (NSRange)markedRange +{ + return _markedRange; +} + +- (NSRange)selectedRange +{ + return _selectedRange; +} + +- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange +{ + if ([aString isKindOfClass:[NSAttributedString class]]) { + aString = [aString string]; + } + + if ([aString length] == 0) { + [self unmarkText]; + return; + } + + if (_markedText != aString) { + _markedText = aString; + } + + _selectedRange = selectedRange; + _markedRange = NSMakeRange(0, [aString length]); + + // This key event was consumed by the IME + [self clearPendingKey]; + + NSUInteger utf32SelectedRangeLocation = [[aString substringToIndex:selectedRange.location] lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; + NSUInteger utf32SelectionRangeEnd = [[aString substringToIndex:(selectedRange.location + selectedRange.length)] lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; + NSUInteger utf32SelectionRangeLength = utf32SelectionRangeEnd - utf32SelectedRangeLocation; + + SDL_SendEditingText([aString UTF8String], + (int)utf32SelectedRangeLocation, (int)utf32SelectionRangeLength); + + DEBUG_IME(@"setMarkedText: %@, (%d, %d) replacement range (%d, %d)", _markedText, + (int)selectedRange.location, (int)selectedRange.length, + (int)replacementRange.location, (int)replacementRange.length); +} + +- (void)unmarkText +{ + _markedText = nil; + + // This key event was consumed by the IME + [self clearPendingKey]; + + SDL_SendEditingText("", 0, 0); +} + +- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange +{ + NSWindow *window = [self window]; + NSRect contentRect = [window contentRectForFrameRect:[window frame]]; + float windowHeight = contentRect.size.height; + NSRect rect = NSMakeRect(_inputRect.x, windowHeight - _inputRect.y - _inputRect.h, + _inputRect.w, _inputRect.h); + + if (actualRange) { + *actualRange = aRange; + } + + DEBUG_IME(@"firstRectForCharacterRange: (%d, %d): windowHeight = %g, rect = %@", + (int)aRange.location, (int)aRange.length, windowHeight, + NSStringFromRect(rect)); + + rect = [window convertRectToScreen:rect]; + + return rect; +} + +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange +{ + DEBUG_IME(@"attributedSubstringFromRange: (%d, %d)", (int)aRange.location, (int)aRange.length); + return nil; +} + +- (NSInteger)conversationIdentifier +{ + return (NSInteger)self; +} + +/* This method returns the index for character that is + * nearest to thePoint. thPoint is in screen coordinate system. + */ +- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint +{ + DEBUG_IME(@"characterIndexForPoint: (%g, %g)", thePoint.x, thePoint.y); + return 0; +} + +/* This method is the key to attribute extension. + * We could add new attributes through this method. + * NSInputServer examines the return value of this + * method & constructs appropriate attributed string. + */ +- (NSArray *)validAttributesForMarkedText +{ + return [NSArray array]; +} + +- (void)setPendingKey:(int)rawcode scancode:(SDL_Scancode)scancode timestamp:(Uint64)timestamp +{ + _pendingRawCode = rawcode; + _pendingScancode = scancode; + _pendingTimestamp = timestamp; +} + +- (void)sendPendingKey +{ + if (_pendingRawCode < 0) { + return; + } + + SDL_SendKeyboardKey(_pendingTimestamp, SDL_DEFAULT_KEYBOARD_ID, _pendingRawCode, _pendingScancode, true); + [self clearPendingKey]; +} + +- (void)clearPendingKey +{ + _pendingRawCode = -1; +} + +@end + +static bool IsModifierKeyPressed(unsigned int flags, + unsigned int target_mask, + unsigned int other_mask, + unsigned int either_mask) +{ + bool target_pressed = (flags & target_mask) != 0; + bool other_pressed = (flags & other_mask) != 0; + bool either_pressed = (flags & either_mask) != 0; + + if (either_pressed != (target_pressed || other_pressed)) + return either_pressed; + + return target_pressed; +} + +static void HandleModifiers(SDL_VideoDevice *_this, SDL_Scancode code, unsigned int modifierFlags) +{ + bool pressed = false; + + if (code == SDL_SCANCODE_LSHIFT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELSHIFTKEYMASK, + NX_DEVICERSHIFTKEYMASK, NX_SHIFTMASK); + } else if (code == SDL_SCANCODE_LCTRL) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELCTLKEYMASK, + NX_DEVICERCTLKEYMASK, NX_CONTROLMASK); + } else if (code == SDL_SCANCODE_LALT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELALTKEYMASK, + NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK); + } else if (code == SDL_SCANCODE_LGUI) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELCMDKEYMASK, + NX_DEVICERCMDKEYMASK, NX_COMMANDMASK); + } else if (code == SDL_SCANCODE_RSHIFT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERSHIFTKEYMASK, + NX_DEVICELSHIFTKEYMASK, NX_SHIFTMASK); + } else if (code == SDL_SCANCODE_RCTRL) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERCTLKEYMASK, + NX_DEVICELCTLKEYMASK, NX_CONTROLMASK); + } else if (code == SDL_SCANCODE_RALT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERALTKEYMASK, + NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK); + } else if (code == SDL_SCANCODE_RGUI) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERCMDKEYMASK, + NX_DEVICELCMDKEYMASK, NX_COMMANDMASK); + } else { + return; + } + + if (pressed) { + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, code, true); + } else { + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, code, false); + } +} + +static void UpdateKeymap(SDL_CocoaVideoData *data, bool send_event) +{ + TISInputSourceRef key_layout; + UCKeyboardLayout *keyLayoutPtr = NULL; + CFDataRef uchrDataRef; + + // See if the keymap needs to be updated + key_layout = TISCopyCurrentKeyboardLayoutInputSource(); + if (key_layout == data.key_layout) { + return; + } + data.key_layout = key_layout; + + // Try Unicode data first + uchrDataRef = TISGetInputSourceProperty(key_layout, kTISPropertyUnicodeKeyLayoutData); + if (uchrDataRef) { + keyLayoutPtr = (UCKeyboardLayout *)CFDataGetBytePtr(uchrDataRef); + } + + if (!keyLayoutPtr) { + CFRelease(key_layout); + return; + } + + static struct { + int flags; + SDL_Keymod modstate; + } mods[] = { + { 0, SDL_KMOD_NONE }, + { shiftKey, SDL_KMOD_SHIFT }, + { alphaLock, SDL_KMOD_CAPS }, + { (shiftKey | alphaLock), (SDL_KMOD_SHIFT | SDL_KMOD_CAPS) }, + { optionKey, SDL_KMOD_ALT }, + { (optionKey | shiftKey), (SDL_KMOD_ALT | SDL_KMOD_SHIFT) }, + { (optionKey | alphaLock), (SDL_KMOD_ALT | SDL_KMOD_CAPS) }, + { (optionKey | shiftKey | alphaLock), (SDL_KMOD_ALT | SDL_KMOD_SHIFT | SDL_KMOD_CAPS) } + }; + + UInt32 keyboard_type = LMGetKbdType(); + + SDL_Keymap *keymap = SDL_CreateKeymap(); + for (int m = 0; m < SDL_arraysize(mods); ++m) { + for (int i = 0; i < SDL_arraysize(darwin_scancode_table); i++) { + OSStatus err; + UniChar s[8]; + UniCharCount len; + UInt32 dead_key_state; + + // Make sure this scancode is a valid character scancode + SDL_Scancode scancode = darwin_scancode_table[i]; + if (scancode == SDL_SCANCODE_UNKNOWN || + scancode == SDL_SCANCODE_DELETE || + (SDL_GetKeymapKeycode(NULL, scancode, SDL_KMOD_NONE) & SDLK_SCANCODE_MASK)) { + continue; + } + + /* + * Swap the scancode for these two wrongly translated keys + * UCKeyTranslate() function does not do its job properly for ISO layout keyboards, where the key '@', + * which is located in the top left corner of the keyboard right under the Escape key, and the additional + * key '<', which is on the right of the Shift key, are inverted + */ + if ((scancode == SDL_SCANCODE_NONUSBACKSLASH || scancode == SDL_SCANCODE_GRAVE) && KBGetLayoutType(LMGetKbdType()) == kKeyboardISO) { + // see comments in scancodes_darwin.h + scancode = (SDL_Scancode)((SDL_SCANCODE_NONUSBACKSLASH + SDL_SCANCODE_GRAVE) - scancode); + } + + dead_key_state = 0; + err = UCKeyTranslate(keyLayoutPtr, i, kUCKeyActionDown, + ((mods[m].flags >> 8) & 0xFF), keyboard_type, + kUCKeyTranslateNoDeadKeysMask, + &dead_key_state, 8, &len, s); + if (err != noErr) { + continue; + } + + if (len > 0 && s[0] != 0x10) { + SDL_SetKeymapEntry(keymap, scancode, mods[m].modstate, s[0]); + } else { + // The default keymap doesn't have any SDL_KMOD_ALT entries, so we don't need to override them + if (!(mods[m].modstate & SDL_KMOD_ALT)) { + SDL_SetKeymapEntry(keymap, scancode, mods[m].modstate, SDLK_UNKNOWN); + } + } + } + } + SDL_SetKeymap(keymap, send_event); +} + +static void SDLCALL SDL_MacOptionAsAltChanged(void *userdata, const char *name, const char *oldValue, const char *hint) +{ + SDL_VideoDevice *_this = (SDL_VideoDevice *)userdata; + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + + if (hint && *hint) { + if (SDL_strcmp(hint, "none") == 0) { + data.option_as_alt = OptionAsAltNone; + } else if (SDL_strcmp(hint, "only_left") == 0) { + data.option_as_alt = OptionAsAltOnlyLeft; + } else if (SDL_strcmp(hint, "only_right") == 0) { + data.option_as_alt = OptionAsAltOnlyRight; + } else if (SDL_strcmp(hint, "both") == 0) { + data.option_as_alt = OptionAsAltBoth; + } + } else { + data.option_as_alt = OptionAsAltNone; + } +} + +void Cocoa_InitKeyboard(SDL_VideoDevice *_this) +{ + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + + UpdateKeymap(data, false); + + // Set our own names for the platform-dependent but layout-independent keys + // This key is NumLock on the MacBook keyboard. :) + // SDL_SetScancodeName(SDL_SCANCODE_NUMLOCKCLEAR, "Clear"); + SDL_SetScancodeName(SDL_SCANCODE_LALT, "Left Option"); + SDL_SetScancodeName(SDL_SCANCODE_LGUI, "Left Command"); + SDL_SetScancodeName(SDL_SCANCODE_RALT, "Right Option"); + SDL_SetScancodeName(SDL_SCANCODE_RGUI, "Right Command"); + + data.modifierFlags = (unsigned int)[NSEvent modifierFlags]; + SDL_ToggleModState(SDL_KMOD_CAPS, (data.modifierFlags & NSEventModifierFlagCapsLock) ? true : false); + + SDL_AddHintCallback(SDL_HINT_MAC_OPTION_AS_ALT, SDL_MacOptionAsAltChanged, _this); +} + +bool Cocoa_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props) +{ + @autoreleasepool { + NSView *parentView; + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + + parentView = [nswindow contentView]; + + /* We only keep one field editor per process, since only the front most + * window can receive text input events, so it make no sense to keep more + * than one copy. When we switched to another window and requesting for + * text input, simply remove the field editor from its superview then add + * it to the front most window's content view */ + if (!data.fieldEdit) { + data.fieldEdit = [[SDL3TranslatorResponder alloc] initWithFrame:NSMakeRect(0.0, 0.0, 0.0, 0.0)]; + } + + if (![[data.fieldEdit superview] isEqual:parentView]) { + // DEBUG_IME(@"add fieldEdit to window contentView"); + [data.fieldEdit removeFromSuperview]; + [parentView addSubview:data.fieldEdit]; + [nswindow makeFirstResponder:data.fieldEdit]; + } + } + return Cocoa_UpdateTextInputArea(_this, window); +} + +bool Cocoa_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + + if (data && data.fieldEdit) { + [data.fieldEdit removeFromSuperview]; + data.fieldEdit = nil; + } + } + return true; +} + +bool Cocoa_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window) +{ + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + if (data.fieldEdit) { + [data.fieldEdit setInputRect:&window->text_input_rect]; + } + return true; +} + +static NSEvent *ReplaceEvent(NSEvent *event, OptionAsAlt option_as_alt) +{ + if (option_as_alt == OptionAsAltNone) { + return event; + } + + const unsigned int modflags = (unsigned int)[event modifierFlags]; + + bool ignore_alt_characters = false; + + bool lalt_pressed = IsModifierKeyPressed(modflags, NX_DEVICELALTKEYMASK, + NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK); + bool ralt_pressed = IsModifierKeyPressed(modflags, NX_DEVICERALTKEYMASK, + NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK); + + if (option_as_alt == OptionAsAltOnlyLeft && lalt_pressed) { + ignore_alt_characters = true; + } else if (option_as_alt == OptionAsAltOnlyRight && ralt_pressed) { + ignore_alt_characters = true; + } else if (option_as_alt == OptionAsAltBoth && (lalt_pressed || ralt_pressed)) { + ignore_alt_characters = true; + } + + bool cmd_pressed = modflags & NX_COMMANDMASK; + bool ctrl_pressed = modflags & NX_CONTROLMASK; + + ignore_alt_characters = ignore_alt_characters && !cmd_pressed && !ctrl_pressed; + + if (ignore_alt_characters) { + NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers]; + return [NSEvent keyEventWithType:[event type] + location:[event locationInWindow] + modifierFlags:modflags + timestamp:[event timestamp] + windowNumber:[event windowNumber] + context:nil + characters:charactersIgnoringModifiers + charactersIgnoringModifiers:charactersIgnoringModifiers + isARepeat:[event isARepeat] + keyCode:[event keyCode]]; + } + + return event; +} + +void Cocoa_HandleKeyEvent(SDL_VideoDevice *_this, NSEvent *event) +{ + unsigned short scancode; + SDL_Scancode code; + SDL_CocoaVideoData *data = _this ? ((__bridge SDL_CocoaVideoData *)_this->internal) : nil; + if (!data) { + return; // can happen when returning from fullscreen Space on shutdown + } + + if ([event type] == NSEventTypeKeyDown || [event type] == NSEventTypeKeyUp) { + event = ReplaceEvent(event, data.option_as_alt); + } + + scancode = [event keyCode]; + + if ((scancode == 10 || scancode == 50) && KBGetLayoutType(LMGetKbdType()) == kKeyboardISO) { + // see comments in scancodes_darwin.h + scancode = 60 - scancode; + } + + if (scancode < SDL_arraysize(darwin_scancode_table)) { + code = darwin_scancode_table[scancode]; + } else { + // Hmm, does this ever happen? If so, need to extend the keymap... + code = SDL_SCANCODE_UNKNOWN; + } + + switch ([event type]) { + case NSEventTypeKeyDown: + if (![event isARepeat]) { + // See if we need to rebuild the keyboard layout + UpdateKeymap(data, true); + } + +#ifdef DEBUG_SCANCODES + if (code == SDL_SCANCODE_UNKNOWN) { + 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 or to Christian Walther . Mac virtual key code is %d.", scancode); + } +#endif + if (SDL_TextInputActive(SDL_GetKeyboardFocus())) { + [data.fieldEdit setPendingKey:scancode scancode:code timestamp:Cocoa_GetEventTimestamp([event timestamp])]; + [data.fieldEdit interpretKeyEvents:[NSArray arrayWithObject:event]]; + [data.fieldEdit sendPendingKey]; + } else if (SDL_GetKeyboardFocus()) { + SDL_SendKeyboardKey(Cocoa_GetEventTimestamp([event timestamp]), SDL_DEFAULT_KEYBOARD_ID, scancode, code, true); + } + break; + case NSEventTypeKeyUp: + SDL_SendKeyboardKey(Cocoa_GetEventTimestamp([event timestamp]), SDL_DEFAULT_KEYBOARD_ID, scancode, code, false); + break; + case NSEventTypeFlagsChanged: { + // see if the new modifierFlags mean any existing keys should be pressed/released... + const unsigned int modflags = (unsigned int)[event modifierFlags]; + HandleModifiers(_this, SDL_SCANCODE_LSHIFT, modflags); + HandleModifiers(_this, SDL_SCANCODE_LCTRL, modflags); + HandleModifiers(_this, SDL_SCANCODE_LALT, modflags); + HandleModifiers(_this, SDL_SCANCODE_LGUI, modflags); + HandleModifiers(_this, SDL_SCANCODE_RSHIFT, modflags); + HandleModifiers(_this, SDL_SCANCODE_RCTRL, modflags); + HandleModifiers(_this, SDL_SCANCODE_RALT, modflags); + HandleModifiers(_this, SDL_SCANCODE_RGUI, modflags); + break; + } + default: // just to avoid compiler warnings + break; + } +} + +void Cocoa_QuitKeyboard(SDL_VideoDevice *_this) +{ +} + +typedef int CGSConnection; +typedef enum +{ + CGSGlobalHotKeyEnable = 0, + CGSGlobalHotKeyDisable = 1, +} CGSGlobalHotKeyOperatingMode; + +extern CGSConnection _CGSDefaultConnection(void); +extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection, CGSGlobalHotKeyOperatingMode mode); + +bool Cocoa_SetWindowKeyboardGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed) +{ +#ifdef SDL_MAC_NO_SANDBOX + CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), grabbed ? CGSGlobalHotKeyDisable : CGSGlobalHotKeyEnable); +#endif + return true; +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +extern bool Cocoa_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoavideo.h" + +@interface SDL3MessageBoxPresenter : NSObject +{ + @public + NSInteger clicked; + NSWindow *nswindow; +} +- (id)initWithParentWindow:(SDL_Window *)window; +@end + +@implementation SDL3MessageBoxPresenter +- (id)initWithParentWindow:(SDL_Window *)window +{ + self = [super init]; + if (self) { + clicked = -1; + + // Retain the NSWindow because we'll show the alert later on the main thread + if (window) { + nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + } else { + nswindow = nil; + } + } + + return self; +} + +- (void)showAlert:(NSAlert *)alert +{ + if (nswindow) { + [alert beginSheetModalForWindow:nswindow + completionHandler:^(NSModalResponse returnCode) { + [NSApp stopModalWithCode:returnCode]; + }]; + clicked = [NSApp runModalForWindow:nswindow]; + nswindow = nil; + } else { + clicked = [alert runModal]; + } +} +@end + +static void Cocoa_ShowMessageBoxImpl(const SDL_MessageBoxData *messageboxdata, int *buttonID, bool *result) +{ + NSAlert *alert; + const SDL_MessageBoxButtonData *buttons = messageboxdata->buttons; + SDL3MessageBoxPresenter *presenter; + NSInteger clicked; + int i; + Cocoa_RegisterApp(); + + alert = [[NSAlert alloc] init]; + + if (messageboxdata->flags & SDL_MESSAGEBOX_ERROR) { + [alert setAlertStyle:NSAlertStyleCritical]; + } else if (messageboxdata->flags & SDL_MESSAGEBOX_WARNING) { + [alert setAlertStyle:NSAlertStyleWarning]; + } else { + [alert setAlertStyle:NSAlertStyleInformational]; + } + + [alert setMessageText:[NSString stringWithUTF8String:messageboxdata->title]]; + [alert setInformativeText:[NSString stringWithUTF8String:messageboxdata->message]]; + + for (i = 0; i < messageboxdata->numbuttons; ++i) { + const SDL_MessageBoxButtonData *sdlButton; + NSButton *button; + + if (messageboxdata->flags & SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT) { + sdlButton = &messageboxdata->buttons[messageboxdata->numbuttons - 1 - i]; + } else { + sdlButton = &messageboxdata->buttons[i]; + } + + button = [alert addButtonWithTitle:[NSString stringWithUTF8String:sdlButton->text]]; + if (sdlButton->flags & SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT) { + [button setKeyEquivalent:@"\r"]; + } else if (sdlButton->flags & SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT) { + [button setKeyEquivalent:@"\033"]; + } else { + [button setKeyEquivalent:@""]; + } + } + + presenter = [[SDL3MessageBoxPresenter alloc] initWithParentWindow:messageboxdata->window]; + + [presenter showAlert:alert]; + + clicked = presenter->clicked; + if (clicked >= NSAlertFirstButtonReturn) { + clicked -= NSAlertFirstButtonReturn; + if (messageboxdata->flags & SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT) { + clicked = messageboxdata->numbuttons - 1 - clicked; + } + *buttonID = buttons[clicked].buttonID; + *result = true; + } else { + *result = SDL_SetError("Did not get a valid `clicked button' id: %ld", (long)clicked); + } +} + +// Display a Cocoa message box +bool Cocoa_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID) +{ + @autoreleasepool { + __block bool result = 0; + + if ([NSThread isMainThread]) { + Cocoa_ShowMessageBoxImpl(messageboxdata, buttonID, &result); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + Cocoa_ShowMessageBoxImpl(messageboxdata, buttonID, &result); + }); + } + return result; + } +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +/* + * @author Mark Callow, www.edgewise-consulting.com. + * + * Thanks to @slime73 on GitHub for their gist showing how to add a CAMetalLayer + * backed view. + */ +#include "SDL_internal.h" + +#ifndef SDL_cocoametalview_h_ +#define SDL_cocoametalview_h_ + +#if defined(SDL_VIDEO_DRIVER_COCOA) && (defined(SDL_VIDEO_VULKAN) || defined(SDL_VIDEO_METAL)) + +#import "../SDL_sysvideo.h" + +#import "SDL_cocoawindow.h" + +#import +#import +#import + +@interface SDL3_cocoametalview : NSView + +- (instancetype)initWithFrame:(NSRect)frame + highDPI:(BOOL)highDPI + windowID:(Uint32)windowID + opaque:(BOOL)opaque; + +- (void)updateDrawableSize; +- (NSView *)hitTest:(NSPoint)point; + +// Override superclass tag so this class can set it. +@property(assign, readonly) NSInteger tag; + +@property(nonatomic) BOOL highDPI; +@property(nonatomic) Uint32 sdlWindowID; + +@end + +SDL_MetalView Cocoa_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window); +void Cocoa_Metal_DestroyView(SDL_VideoDevice *_this, SDL_MetalView view); +void *Cocoa_Metal_GetLayer(SDL_VideoDevice *_this, SDL_MetalView view); + +#endif // SDL_VIDEO_DRIVER_COCOA && (SDL_VIDEO_VULKAN || SDL_VIDEO_METAL) + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +/* + * @author Mark Callow, www.edgewise-consulting.com. + * + * Thanks to @slime73 on GitHub for their gist showing how to add a CAMetalLayer + * backed view. + */ +#include "SDL_internal.h" + +#include "../../events/SDL_windowevents_c.h" + +#import "SDL_cocoametalview.h" + +#if defined(SDL_VIDEO_DRIVER_COCOA) && (defined(SDL_VIDEO_VULKAN) || defined(SDL_VIDEO_METAL)) + +static bool SDLCALL SDL_MetalViewEventWatch(void *userdata, SDL_Event *event) +{ + /* Update the drawable size when SDL receives a size changed event for + * the window that contains the metal view. It would be nice to use + * - (void)resizeWithOldSuperviewSize:(NSSize)oldSize and + * - (void)viewDidChangeBackingProperties instead, but SDL's size change + * events don't always happen in the same frame (for example when a + * resizable window exits a fullscreen Space via the user pressing the OS + * exit-space button). */ + if (event->type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) { + @autoreleasepool { + SDL3_cocoametalview *view = (__bridge SDL3_cocoametalview *)userdata; + if (view.sdlWindowID == event->window.windowID) { + [view updateDrawableSize]; + } + } + } + return false; +} + +@implementation SDL3_cocoametalview + +// Return a Metal-compatible layer. ++ (Class)layerClass +{ + return NSClassFromString(@"CAMetalLayer"); +} + +// Indicate the view wants to draw using a backing layer instead of drawRect. +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +/* When the wantsLayer property is set to YES, this method will be invoked to + * return a layer instance. + */ +- (CALayer *)makeBackingLayer +{ + return [self.class.layerClass layer]; +} + +- (instancetype)initWithFrame:(NSRect)frame + highDPI:(BOOL)highDPI + windowID:(Uint32)windowID + opaque:(BOOL)opaque +{ + self = [super initWithFrame:frame]; + if (self != nil) { + self.highDPI = highDPI; + self.sdlWindowID = windowID; + self.wantsLayer = YES; + + // Allow resize. + self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + + self.layer.opaque = opaque; + + SDL_AddWindowEventWatch(SDL_WINDOW_EVENT_WATCH_EARLY, SDL_MetalViewEventWatch, (__bridge void *)(self)); + + [self updateDrawableSize]; + } + + return self; +} + +- (void)dealloc +{ + SDL_RemoveWindowEventWatch(SDL_WINDOW_EVENT_WATCH_EARLY, SDL_MetalViewEventWatch, (__bridge void *)(self)); +} + +- (NSInteger)tag +{ + return SDL_METALVIEW_TAG; +} + +- (void)updateDrawableSize +{ + CAMetalLayer *metalLayer = (CAMetalLayer *)self.layer; + NSSize size = self.bounds.size; + NSSize backingSize = size; + + if (self.highDPI) { + /* Note: NSHighResolutionCapable must be set to true in the app's + * Info.plist in order for the backing size to be high res. + */ + backingSize = [self convertSizeToBacking:size]; + } + + metalLayer.contentsScale = backingSize.height / size.height; + metalLayer.drawableSize = NSSizeToCGSize(backingSize); +} + +- (NSView *)hitTest:(NSPoint)point +{ + return nil; +} + +@end + +SDL_MetalView Cocoa_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSView *view = data.nswindow.contentView; + BOOL highDPI = (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) != 0; + BOOL opaque = (window->flags & SDL_WINDOW_TRANSPARENT) == 0; + Uint32 windowID = SDL_GetWindowID(window); + SDL3_cocoametalview *newview; + SDL_MetalView metalview; + + newview = [[SDL3_cocoametalview alloc] initWithFrame:view.frame + highDPI:highDPI + windowID:windowID + opaque:opaque]; + if (newview == nil) { + SDL_OutOfMemory(); + return NULL; + } + + [view addSubview:newview]; + + // Make sure the drawable size is up to date after attaching the view. + [newview updateDrawableSize]; + + metalview = (SDL_MetalView)CFBridgingRetain(newview); + + return metalview; + } +} + +void Cocoa_Metal_DestroyView(SDL_VideoDevice *_this, SDL_MetalView view) +{ + @autoreleasepool { + SDL3_cocoametalview *metalview = CFBridgingRelease(view); + [metalview removeFromSuperview]; + } +} + +void *Cocoa_Metal_GetLayer(SDL_VideoDevice *_this, SDL_MetalView view) +{ + @autoreleasepool { + SDL3_cocoametalview *cocoaview = (__bridge SDL3_cocoametalview *)view; + return (__bridge void *)cocoaview.layer; + } +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoamodes_h_ +#define SDL_cocoamodes_h_ + +struct SDL_DisplayData +{ + CGDirectDisplayID display; +}; + +struct SDL_DisplayModeData +{ + CFMutableArrayRef modes; +}; + +extern void Cocoa_InitModes(SDL_VideoDevice *_this); +extern void Cocoa_UpdateDisplays(SDL_VideoDevice *_this); +extern bool Cocoa_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect); +extern bool Cocoa_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect); +extern bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display); +extern bool Cocoa_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode); +extern void Cocoa_QuitModes(SDL_VideoDevice *_this); +extern SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoavideo.h" +#include "../../events/SDL_events_c.h" + +// We need this for IODisplayCreateInfoDictionary and kIODisplayOnlyPreferredName +#include + +// We need this for CVDisplayLinkGetNominalOutputVideoRefreshPeriod +#include +#include + +#if (IOGRAPHICSTYPES_REV < 40) +#define kDisplayModeNativeFlag 0x02000000 +#endif + +static bool CG_SetError(const char *prefix, CGDisplayErr result) +{ + const char *error; + + switch (result) { + case kCGErrorFailure: + error = "kCGErrorFailure"; + break; + case kCGErrorIllegalArgument: + error = "kCGErrorIllegalArgument"; + break; + case kCGErrorInvalidConnection: + error = "kCGErrorInvalidConnection"; + break; + case kCGErrorInvalidContext: + error = "kCGErrorInvalidContext"; + break; + case kCGErrorCannotComplete: + error = "kCGErrorCannotComplete"; + break; + case kCGErrorNotImplemented: + error = "kCGErrorNotImplemented"; + break; + case kCGErrorRangeCheck: + error = "kCGErrorRangeCheck"; + break; + case kCGErrorTypeCheck: + error = "kCGErrorTypeCheck"; + break; + case kCGErrorInvalidOperation: + error = "kCGErrorInvalidOperation"; + break; + case kCGErrorNoneAvailable: + error = "kCGErrorNoneAvailable"; + break; + default: + error = "Unknown Error"; + break; + } + return SDL_SetError("%s: %s", prefix, error); +} + +static NSScreen *GetNSScreenForDisplayID(CGDirectDisplayID displayID) +{ + NSArray *screens = [NSScreen screens]; + + // !!! FIXME: maybe track the NSScreen in SDL_DisplayData? + for (NSScreen *screen in screens) { + const CGDirectDisplayID thisDisplay = (CGDirectDisplayID)[[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue]; + if (thisDisplay == displayID) { + return screen; + } + } + return nil; +} + +SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid) +{ + for (int i = 0; i < _this->num_displays; i++) { + const SDL_DisplayData *displaydata = _this->displays[i]->internal; + if (displaydata && (displaydata->display == displayid)) { + return _this->displays[i]; + } + } + return NULL; +} + +static float GetDisplayModeRefreshRate(CGDisplayModeRef vidmode, CVDisplayLinkRef link) +{ + float refreshRate = (float)CGDisplayModeGetRefreshRate(vidmode); + + // CGDisplayModeGetRefreshRate can return 0 (eg for built-in displays). + if (refreshRate == 0 && link != NULL) { + CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link); + if ((time.flags & kCVTimeIsIndefinite) == 0 && time.timeValue != 0) { + refreshRate = (float)time.timeScale / time.timeValue; + } + } + + return refreshRate; +} + +static bool HasValidDisplayModeFlags(CGDisplayModeRef vidmode) +{ + uint32_t ioflags = CGDisplayModeGetIOFlags(vidmode); + + // Filter out modes which have flags that we don't want. + if (ioflags & (kDisplayModeNeverShowFlag | kDisplayModeNotGraphicsQualityFlag)) { + return false; + } + + // Filter out modes which don't have flags that we want. + if (!(ioflags & kDisplayModeValidFlag) || !(ioflags & kDisplayModeSafeFlag)) { + return false; + } + + return true; +} + +static Uint32 GetDisplayModePixelFormat(CGDisplayModeRef vidmode) +{ + // This API is deprecated in 10.11 with no good replacement (as of 10.15). + CFStringRef fmt = CGDisplayModeCopyPixelEncoding(vidmode); + Uint32 pixelformat = SDL_PIXELFORMAT_UNKNOWN; + + if (CFStringCompare(fmt, CFSTR(IO32BitDirectPixels), + kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + pixelformat = SDL_PIXELFORMAT_ARGB8888; + } else if (CFStringCompare(fmt, CFSTR(IO16BitDirectPixels), + kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + pixelformat = SDL_PIXELFORMAT_ARGB1555; + } else if (CFStringCompare(fmt, CFSTR(kIO30BitDirectPixels), + kCFCompareCaseInsensitive) == kCFCompareEqualTo) { + pixelformat = SDL_PIXELFORMAT_ARGB2101010; + } else { + // ignore 8-bit and such for now. + } + + CFRelease(fmt); + + return pixelformat; +} + +static bool GetDisplayMode(CGDisplayModeRef vidmode, bool vidmodeCurrent, CFArrayRef modelist, CVDisplayLinkRef link, SDL_DisplayMode *mode) +{ + SDL_DisplayModeData *data; + bool usableForGUI = CGDisplayModeIsUsableForDesktopGUI(vidmode); + size_t width = CGDisplayModeGetWidth(vidmode); + size_t height = CGDisplayModeGetHeight(vidmode); + size_t pixelW = width; + size_t pixelH = height; + uint32_t ioflags = CGDisplayModeGetIOFlags(vidmode); + float refreshrate = GetDisplayModeRefreshRate(vidmode, link); + Uint32 format = GetDisplayModePixelFormat(vidmode); + bool interlaced = (ioflags & kDisplayModeInterlacedFlag) != 0; + CFMutableArrayRef modes; + + if (format == SDL_PIXELFORMAT_UNKNOWN) { + return false; + } + + /* Don't fail the current mode based on flags because this could prevent Cocoa_InitModes from + * succeeding if the current mode lacks certain flags (esp kDisplayModeSafeFlag). */ + if (!vidmodeCurrent && !HasValidDisplayModeFlags(vidmode)) { + return false; + } + + modes = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + CFArrayAppendValue(modes, vidmode); + + /* If a list of possible display modes is passed in, use it to filter out + * modes that have duplicate sizes. We don't just rely on SDL's higher level + * duplicate filtering because this code can choose what properties are + * preferred, and it can add CGDisplayModes to the DisplayModeData's list of + * modes to try (see comment below for why that's necessary). */ + pixelW = CGDisplayModeGetPixelWidth(vidmode); + pixelH = CGDisplayModeGetPixelHeight(vidmode); + + if (modelist != NULL) { + CFIndex modescount = CFArrayGetCount(modelist); + int i; + + for (i = 0; i < modescount; i++) { + size_t otherW, otherH, otherpixelW, otherpixelH; + float otherrefresh; + Uint32 otherformat; + bool otherGUI; + CGDisplayModeRef othermode = (CGDisplayModeRef)CFArrayGetValueAtIndex(modelist, i); + uint32_t otherioflags = CGDisplayModeGetIOFlags(othermode); + + if (CFEqual(vidmode, othermode)) { + continue; + } + + if (!HasValidDisplayModeFlags(othermode)) { + continue; + } + + otherW = CGDisplayModeGetWidth(othermode); + otherH = CGDisplayModeGetHeight(othermode); + otherpixelW = CGDisplayModeGetPixelWidth(othermode); + otherpixelH = CGDisplayModeGetPixelHeight(othermode); + otherrefresh = GetDisplayModeRefreshRate(othermode, link); + otherformat = GetDisplayModePixelFormat(othermode); + otherGUI = CGDisplayModeIsUsableForDesktopGUI(othermode); + + /* Ignore this mode if it's interlaced and there's a non-interlaced + * mode in the list with the same properties. + */ + if (interlaced && ((otherioflags & kDisplayModeInterlacedFlag) == 0) && width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && refreshrate == otherrefresh && format == otherformat && usableForGUI == otherGUI) { + CFRelease(modes); + return false; + } + + /* Ignore this mode if it's not usable for desktop UI and its + * properties are equal to another GUI-capable mode in the list. + */ + if (width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && !usableForGUI && otherGUI && refreshrate == otherrefresh && format == otherformat) { + CFRelease(modes); + return false; + } + + /* If multiple modes have the exact same properties, they'll all + * go in the list of modes to try when SetDisplayMode is called. + * This is needed because kCGDisplayShowDuplicateLowResolutionModes + * (which is used to expose highdpi display modes) can make the + * list of modes contain duplicates (according to their properties + * obtained via public APIs) which don't work with SetDisplayMode. + * Those duplicate non-functional modes *do* have different pixel + * formats according to their internal data structure viewed with + * NSLog, but currently no public API can detect that. + * https://bugzilla.libsdl.org/show_bug.cgi?id=4822 + * + * As of macOS 10.15.0, those duplicates have the exact same + * properties via public APIs in every way (even their IO flags and + * CGDisplayModeGetIODisplayModeID is the same), so we could test + * those for equality here too, but I'm intentionally not doing that + * in case there are duplicate modes with different IO flags or IO + * display mode IDs in the future. In that case I think it's better + * to try them all in SetDisplayMode than to risk one of them being + * correct but it being filtered out by SDL_AddFullscreenDisplayMode + * as being a duplicate. + */ + if (width == otherW && height == otherH && pixelW == otherpixelW && pixelH == otherpixelH && usableForGUI == otherGUI && refreshrate == otherrefresh && format == otherformat) { + CFArrayAppendValue(modes, othermode); + } + } + } + + SDL_zerop(mode); + data = (SDL_DisplayModeData *)SDL_malloc(sizeof(*data)); + if (!data) { + CFRelease(modes); + return false; + } + data->modes = modes; + mode->format = format; + mode->w = (int)width; + mode->h = (int)height; + mode->pixel_density = (float)pixelW / width; + mode->refresh_rate = refreshrate; + mode->internal = data; + return true; +} + +static char *Cocoa_GetDisplayName(CGDirectDisplayID displayID) +{ + if (@available(macOS 10.15, *)) { + NSScreen *screen = GetNSScreenForDisplayID(displayID); + if (screen) { + const char *name = [screen.localizedName UTF8String]; + if (name) { + return SDL_strdup(name); + } + } + } + + // This API is deprecated in 10.9 with no good replacement (as of 10.15). + io_service_t servicePort = CGDisplayIOServicePort(displayID); + CFDictionaryRef deviceInfo = IODisplayCreateInfoDictionary(servicePort, kIODisplayOnlyPreferredName); + NSDictionary *localizedNames = [(__bridge NSDictionary *)deviceInfo objectForKey:[NSString stringWithUTF8String:kDisplayProductName]]; + char *displayName = NULL; + + if ([localizedNames count] > 0) { + displayName = SDL_strdup([[localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]] UTF8String]); + } + CFRelease(deviceInfo); + return displayName; +} + +static void Cocoa_GetHDRProperties(CGDirectDisplayID displayID, SDL_HDROutputProperties *HDR) +{ + HDR->SDR_white_level = 1.0f; + HDR->HDR_headroom = 1.0f; + + if (@available(macOS 10.15, *)) { + NSScreen *screen = GetNSScreenForDisplayID(displayID); + if (screen) { + if (screen.maximumExtendedDynamicRangeColorComponentValue > 1.0f) { + HDR->HDR_headroom = screen.maximumExtendedDynamicRangeColorComponentValue; + } else { + HDR->HDR_headroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue; + } + } + } +} + + +bool Cocoa_AddDisplay(CGDirectDisplayID display, bool send_event) +{ + CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(display); + if (!moderef) { + return false; + } + + SDL_DisplayData *displaydata = (SDL_DisplayData *)SDL_malloc(sizeof(*displaydata)); + if (!displaydata) { + CGDisplayModeRelease(moderef); + return false; + } + displaydata->display = display; + + CVDisplayLinkRef link = NULL; + CVDisplayLinkCreateWithCGDisplay(display, &link); + + SDL_VideoDisplay viddisplay; + SDL_zero(viddisplay); + viddisplay.name = Cocoa_GetDisplayName(display); // this returns a strdup'ed string + + SDL_DisplayMode mode; + if (!GetDisplayMode(moderef, true, NULL, link, &mode)) { + CVDisplayLinkRelease(link); + CGDisplayModeRelease(moderef); + SDL_free(viddisplay.name); + SDL_free(displaydata); + return false; + } + + CVDisplayLinkRelease(link); + CGDisplayModeRelease(moderef); + + Cocoa_GetHDRProperties(displaydata->display, &viddisplay.HDR); + + viddisplay.desktop_mode = mode; + viddisplay.internal = displaydata; + const bool retval = SDL_AddVideoDisplay(&viddisplay, send_event); + SDL_free(viddisplay.name); + return retval; +} + +static void Cocoa_DisplayReconfigurationCallback(CGDirectDisplayID displayid, CGDisplayChangeSummaryFlags flags, void *userInfo) +{ + #if 0 + SDL_Log("COCOA DISPLAY RECONFIG CALLBACK! display=%u", (unsigned int) displayid); + #define CHECK_DISPLAY_RECONFIG_FLAG(x) if (flags & x) { SDL_Log(" - " #x); } + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayBeginConfigurationFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMovedFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetMainFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetModeFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayAddFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayRemoveFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayEnabledFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDisabledFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMirrorFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayUnMirrorFlag); + CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDesktopShapeChangedFlag); + #undef CHECK_DISPLAY_RECONFIG_FLAG + #endif + + SDL_VideoDevice *_this = (SDL_VideoDevice *) userInfo; + SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid); // will be NULL for newly-added (or newly-unmirrored) displays! + + if (flags & kCGDisplayDisabledFlag) { + flags |= kCGDisplayRemoveFlag; // treat this like a display leaving, even though it's still plugged in. + } + + if (flags & kCGDisplayEnabledFlag) { + flags |= kCGDisplayAddFlag; // treat this like a display leaving, even though it's still plugged in. + } + + if (flags & kCGDisplayMirrorFlag) { + flags |= kCGDisplayRemoveFlag; // treat this like a display leaving, even though it's still actually here. + } + + if (flags & kCGDisplayUnMirrorFlag) { + flags |= kCGDisplayAddFlag; // treat this like a new display arriving, even though it was here all along. + } + + if ((flags & kCGDisplayAddFlag) && (flags & kCGDisplayRemoveFlag)) { + // 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. + // Mirrored things are always removed, since they don't represent a discrete display in this state. + if (((flags & kCGDisplayMirrorFlag) == 0) && (CGDisplayPixelsWide(displayid) > 1)) { + // Final state is connected + flags &= ~kCGDisplayRemoveFlag; + } else { + // Final state is disconnected + flags &= ~kCGDisplayAddFlag; + } + } + + if (flags & kCGDisplayAddFlag) { + if (!display) { + if (!Cocoa_AddDisplay(displayid, true)) { + return; // oh well. + } + display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid); + SDL_assert(display != NULL); + } + } + + if (flags & kCGDisplayRemoveFlag) { + if (display) { + SDL_DelVideoDisplay(display->id, true); + display = NULL; + } + } + + if (flags & kCGDisplaySetModeFlag) { + if (display) { + CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(displayid); + if (moderef) { + CVDisplayLinkRef link = NULL; + CVDisplayLinkCreateWithCGDisplay(displayid, &link); + if (link) { + SDL_DisplayMode mode; + if (GetDisplayMode(moderef, true, NULL, link, &mode)) { + SDL_SetDesktopDisplayMode(display, &mode); + } + CVDisplayLinkRelease(link); + } + CGDisplayModeRelease(moderef); + } + } + } + + if (flags & kCGDisplaySetMainFlag) { + if (display) { + for (int i = 0; i < _this->num_displays; i++) { + if (_this->displays[i] == display) { + if (i > 0) { + // move this display to the front of _this->displays so it's treated as primary. + SDL_memmove(&_this->displays[1], &_this->displays[0], sizeof (*_this->displays) * i); + _this->displays[0] = display; + } + flags |= kCGDisplayMovedFlag; // we don't have an SDL event atm for "this display became primary," so at least let everyone know it "moved". + break; + } + } + } + } + + if (flags & kCGDisplayMovedFlag) { + if (display) { + SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_MOVED, 0, 0); + } + } + + if (flags & kCGDisplayDesktopShapeChangedFlag) { + SDL_UpdateDesktopBounds(); + } +} + +void Cocoa_InitModes(SDL_VideoDevice *_this) +{ + @autoreleasepool { + CGDisplayErr result; + CGDisplayCount numDisplays = 0; + + result = CGGetOnlineDisplayList(0, NULL, &numDisplays); + if (result != kCGErrorSuccess) { + CG_SetError("CGGetOnlineDisplayList()", result); + return; + } + + bool isstack; + CGDirectDisplayID *displays = SDL_small_alloc(CGDirectDisplayID, numDisplays, &isstack); + + result = CGGetOnlineDisplayList(numDisplays, displays, &numDisplays); + if (result != kCGErrorSuccess) { + CG_SetError("CGGetOnlineDisplayList()", result); + SDL_small_free(displays, isstack); + return; + } + + // future updates to the display graph will come through this callback. + CGDisplayRegisterReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this); + + // Pick up the primary display in the first pass, then get the rest + for (int pass = 0; pass < 2; ++pass) { + for (int i = 0; i < numDisplays; ++i) { + if (pass == 0) { + if (!CGDisplayIsMain(displays[i])) { + continue; + } + } else { + if (CGDisplayIsMain(displays[i])) { + continue; + } + } + + if (CGDisplayMirrorsDisplay(displays[i]) != kCGNullDirectDisplay) { + continue; + } + + Cocoa_AddDisplay(displays[i], false); + } + } + SDL_small_free(displays, isstack); + } +} + +void Cocoa_UpdateDisplays(SDL_VideoDevice *_this) +{ + SDL_HDROutputProperties HDR; + int i; + + for (i = 0; i < _this->num_displays; ++i) { + SDL_VideoDisplay *display = _this->displays[i]; + SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal; + + Cocoa_GetHDRProperties(displaydata->display, &HDR); + SDL_SetDisplayHDRProperties(display, &HDR); + } +} + +bool Cocoa_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect) +{ + SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal; + CGRect cgrect; + + cgrect = CGDisplayBounds(displaydata->display); + rect->x = (int)cgrect.origin.x; + rect->y = (int)cgrect.origin.y; + rect->w = (int)cgrect.size.width; + rect->h = (int)cgrect.size.height; + return true; +} + +bool Cocoa_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect) +{ + SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal; + NSScreen *screen = GetNSScreenForDisplayID(displaydata->display); + + if (screen == nil) { + return SDL_SetError("Couldn't get NSScreen for display"); + } + + { + const NSRect frame = [screen visibleFrame]; + rect->x = (int)frame.origin.x; + rect->y = (int)(CGDisplayPixelsHigh(kCGDirectMainDisplay) - frame.origin.y - frame.size.height); + rect->w = (int)frame.size.width; + rect->h = (int)frame.size.height; + } + + return true; +} + +bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display) +{ + SDL_DisplayData *data = (SDL_DisplayData *)display->internal; + CVDisplayLinkRef link = NULL; + CFArrayRef modes; + CFDictionaryRef dict = NULL; + const CFStringRef dictkeys[] = { kCGDisplayShowDuplicateLowResolutionModes }; + const CFBooleanRef dictvalues[] = { kCFBooleanTrue }; + + CVDisplayLinkCreateWithCGDisplay(data->display, &link); + + /* By default, CGDisplayCopyAllDisplayModes will only get a subset of the + * system's available modes. For example on a 15" 2016 MBP, users can + * choose 1920x1080@2x in System Preferences but it won't show up here, + * unless we specify the option below. + * The display modes returned by CGDisplayCopyAllDisplayModes are also not + * high dpi-capable unless this option is set. + * macOS 10.15 also seems to have a bug where entering, exiting, and + * re-entering exclusive fullscreen with a low dpi display mode can cause + * the content of the screen to move up, which this setting avoids: + * https://bugzilla.libsdl.org/show_bug.cgi?id=4822 + */ + + dict = CFDictionaryCreate(NULL, + (const void **)dictkeys, + (const void **)dictvalues, + 1, + &kCFCopyStringDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + + modes = CGDisplayCopyAllDisplayModes(data->display, dict); + + if (dict) { + CFRelease(dict); + } + + if (modes) { + CFIndex i; + const CFIndex count = CFArrayGetCount(modes); + + for (i = 0; i < count; i++) { + CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i); + SDL_DisplayMode mode; + + if (GetDisplayMode(moderef, false, modes, link, &mode)) { + if (!SDL_AddFullscreenDisplayMode(display, &mode)) { + CFRelease(mode.internal->modes); + SDL_free(mode.internal); + } + } + } + + CFRelease(modes); + } + + CVDisplayLinkRelease(link); + return true; +} + +static CGError SetDisplayModeForDisplay(CGDirectDisplayID display, SDL_DisplayModeData *data) +{ + /* SDL_DisplayModeData can contain multiple CGDisplayModes to try (with + * identical properties), some of which might not work. See GetDisplayMode. + */ + CGError result = kCGErrorFailure; + for (CFIndex i = 0; i < CFArrayGetCount(data->modes); i++) { + CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(data->modes, i); + result = CGDisplaySetDisplayMode(display, moderef, NULL); + if (result == kCGErrorSuccess) { + // If this mode works, try it first next time. + if (i > 0) { + CFArrayExchangeValuesAtIndices(data->modes, i, 0); + } + break; + } + } + return result; +} + +bool Cocoa_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode) +{ + SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal; + SDL_DisplayModeData *data = mode->internal; + CGDisplayFadeReservationToken fade_token = kCGDisplayFadeReservationInvalidToken; + CGError result = kCGErrorSuccess; + + b_inModeTransition = true; + + // Fade to black to hide resolution-switching flicker + if (CGAcquireDisplayFadeReservation(5, &fade_token) == kCGErrorSuccess) { + CGDisplayFade(fade_token, 0.3, kCGDisplayBlendNormal, kCGDisplayBlendSolidColor, 0.0, 0.0, 0.0, TRUE); + } + + if (data == display->desktop_mode.internal) { + // Restoring desktop mode + SetDisplayModeForDisplay(displaydata->display, data); + } else { + // Do the physical switch + result = SetDisplayModeForDisplay(displaydata->display, data); + } + + // Fade in again (asynchronously) + if (fade_token != kCGDisplayFadeReservationInvalidToken) { + CGDisplayFade(fade_token, 0.5, kCGDisplayBlendSolidColor, kCGDisplayBlendNormal, 0.0, 0.0, 0.0, FALSE); + CGReleaseDisplayFadeReservation(fade_token); + } + + b_inModeTransition = false; + + if (result != kCGErrorSuccess) { + return CG_SetError("CGDisplaySwitchToMode()", result); + } + return true; +} + +void Cocoa_QuitModes(SDL_VideoDevice *_this) +{ + int i, j; + + CGDisplayRemoveReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this); + + for (i = 0; i < _this->num_displays; ++i) { + SDL_VideoDisplay *display = _this->displays[i]; + SDL_DisplayModeData *mode; + + if (display->current_mode->internal != display->desktop_mode.internal) { + Cocoa_SetDisplayMode(_this, display, &display->desktop_mode); + } + + mode = display->desktop_mode.internal; + CFRelease(mode->modes); + + for (j = 0; j < display->num_fullscreen_modes; j++) { + mode = display->fullscreen_modes[j].internal; + CFRelease(mode->modes); + } + } +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoamouse_h_ +#define SDL_cocoamouse_h_ + +#include "SDL_cocoavideo.h" + +extern bool Cocoa_InitMouse(SDL_VideoDevice *_this); +extern NSWindow *Cocoa_GetMouseFocus(); +extern void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event); +extern void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event); +extern void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y); +extern void Cocoa_QuitMouse(SDL_VideoDevice *_this); + +typedef struct +{ + // Whether we've seen a cursor warp since the last move event. + bool seenWarp; + // What location our last cursor warp was to. + CGFloat lastWarpX; + CGFloat lastWarpY; + // What location we last saw the cursor move to. + CGFloat lastMoveX; + CGFloat lastMoveY; +} SDL_MouseData; + +@interface NSCursor (InvisibleCursor) ++ (NSCursor *)invisibleCursor; +@end + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoamouse.h" +#include "SDL_cocoavideo.h" + +#include "../../events/SDL_mouse_c.h" + +#if 0 +#define DEBUG_COCOAMOUSE +#endif + +#ifdef DEBUG_COCOAMOUSE +#define DLog(fmt, ...) printf("%s: " fmt "\n", __func__, ##__VA_ARGS__) +#else +#define DLog(...) \ + do { \ + } while (0) +#endif + +@implementation NSCursor (InvisibleCursor) ++ (NSCursor *)invisibleCursor +{ + static NSCursor *invisibleCursor = NULL; + if (!invisibleCursor) { + // RAW 16x16 transparent GIF + static unsigned char cursorBytes[] = { + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x8C, 0x8F, 0xA9, 0xCB, 0xED, + 0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B + }; + + NSData *cursorData = [NSData dataWithBytesNoCopy:&cursorBytes[0] + length:sizeof(cursorBytes) + freeWhenDone:NO]; + NSImage *cursorImage = [[NSImage alloc] initWithData:cursorData]; + invisibleCursor = [[NSCursor alloc] initWithImage:cursorImage + hotSpot:NSZeroPoint]; + } + + return invisibleCursor; +} +@end + +static SDL_Cursor *Cocoa_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y) +{ + @autoreleasepool { + NSImage *nsimage; + NSCursor *nscursor = NULL; + SDL_Cursor *cursor = NULL; + + nsimage = Cocoa_CreateImage(surface); + if (nsimage) { + nscursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSMakePoint(hot_x, hot_y)]; + } + + if (nscursor) { + cursor = SDL_calloc(1, sizeof(*cursor)); + if (cursor) { + cursor->internal = (void *)CFBridgingRetain(nscursor); + } + } + + return cursor; + } +} + +/* there are .pdf files of some of the cursors we need, installed by default on macOS, but not available through NSCursor. + If we can load them ourselves, use them, otherwise fallback to something standard but not super-great. + Since these are under /System, they should be available even to sandboxed apps. */ +static NSCursor *LoadHiddenSystemCursor(NSString *cursorName, SEL fallback) +{ + NSString *cursorPath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:cursorName]; + NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"info.plist"]]; + // we can't do animation atm. :/ + const int frames = (int)[[info valueForKey:@"frames"] integerValue]; + NSCursor *cursor; + NSImage *image = [[NSImage alloc] initWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"cursor.pdf"]]; + if ((image == nil) || (image.isValid == NO)) { + return [NSCursor performSelector:fallback]; + } + + if (frames > 1) { +#ifdef MAC_OS_VERSION_12_0 // same value as deprecated symbol. + const NSCompositingOperation operation = NSCompositingOperationCopy; +#else + const NSCompositingOperation operation = NSCompositeCopy; +#endif + const NSSize cropped_size = NSMakeSize(image.size.width, (int)(image.size.height / frames)); + NSImage *cropped = [[NSImage alloc] initWithSize:cropped_size]; + if (cropped == nil) { + return [NSCursor performSelector:fallback]; + } + + [cropped lockFocus]; + { + const NSRect cropped_rect = NSMakeRect(0, 0, cropped_size.width, cropped_size.height); + [image drawInRect:cropped_rect fromRect:cropped_rect operation:operation fraction:1]; + } + [cropped unlockFocus]; + image = cropped; + } + + cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint([[info valueForKey:@"hotx"] doubleValue], [[info valueForKey:@"hoty"] doubleValue])]; + return cursor; +} + +static SDL_Cursor *Cocoa_CreateSystemCursor(SDL_SystemCursor id) +{ + @autoreleasepool { + NSCursor *nscursor = NULL; + SDL_Cursor *cursor = NULL; + + switch (id) { + case SDL_SYSTEM_CURSOR_DEFAULT: + nscursor = [NSCursor arrowCursor]; + break; + case SDL_SYSTEM_CURSOR_TEXT: + nscursor = [NSCursor IBeamCursor]; + break; + case SDL_SYSTEM_CURSOR_CROSSHAIR: + nscursor = [NSCursor crosshairCursor]; + break; + case SDL_SYSTEM_CURSOR_WAIT: // !!! FIXME: this is more like WAITARROW + nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor)); + break; + case SDL_SYSTEM_CURSOR_PROGRESS: // !!! FIXME: this is meant to be animated + nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor)); + break; + case SDL_SYSTEM_CURSOR_NWSE_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor)); + break; + case SDL_SYSTEM_CURSOR_NESW_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor)); + break; + case SDL_SYSTEM_CURSOR_EW_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor)); + break; + case SDL_SYSTEM_CURSOR_NS_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor)); + break; + case SDL_SYSTEM_CURSOR_MOVE: + nscursor = LoadHiddenSystemCursor(@"move", @selector(closedHandCursor)); + break; + case SDL_SYSTEM_CURSOR_NOT_ALLOWED: + nscursor = [NSCursor operationNotAllowedCursor]; + break; + case SDL_SYSTEM_CURSOR_POINTER: + nscursor = [NSCursor pointingHandCursor]; + break; + case SDL_SYSTEM_CURSOR_NW_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor)); + break; + case SDL_SYSTEM_CURSOR_N_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor)); + break; + case SDL_SYSTEM_CURSOR_NE_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor)); + break; + case SDL_SYSTEM_CURSOR_E_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor)); + break; + case SDL_SYSTEM_CURSOR_SE_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor)); + break; + case SDL_SYSTEM_CURSOR_S_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor)); + break; + case SDL_SYSTEM_CURSOR_SW_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor)); + break; + case SDL_SYSTEM_CURSOR_W_RESIZE: + nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor)); + break; + default: + SDL_assert(!"Unknown system cursor"); + return NULL; + } + + if (nscursor) { + cursor = SDL_calloc(1, sizeof(*cursor)); + if (cursor) { + // We'll free it later, so retain it here + cursor->internal = (void *)CFBridgingRetain(nscursor); + } + } + + return cursor; + } +} + +static SDL_Cursor *Cocoa_CreateDefaultCursor(void) +{ + SDL_SystemCursor id = SDL_GetDefaultSystemCursor(); + return Cocoa_CreateSystemCursor(id); +} + +static void Cocoa_FreeCursor(SDL_Cursor *cursor) +{ + @autoreleasepool { + CFBridgingRelease((void *)cursor->internal); + SDL_free(cursor); + } +} + +static bool Cocoa_ShowCursor(SDL_Cursor *cursor) +{ + @autoreleasepool { + SDL_VideoDevice *device = SDL_GetVideoDevice(); + SDL_Window *window = (device ? device->windows : NULL); + for (; window != NULL; window = window->next) { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + if (data) { + [data.nswindow performSelectorOnMainThread:@selector(invalidateCursorRectsForView:) + withObject:[data.nswindow contentView] + waitUntilDone:NO]; + } + } + return true; + } +} + +static SDL_Window *SDL_FindWindowAtPoint(const float x, const float y) +{ + const SDL_FPoint pt = { x, y }; + SDL_Window *i; + for (i = SDL_GetVideoDevice()->windows; i; i = i->next) { + const SDL_FRect r = { (float)i->x, (float)i->y, (float)i->w, (float)i->h }; + if (SDL_PointInRectFloat(&pt, &r)) { + return i; + } + } + + return NULL; +} + +static bool Cocoa_WarpMouseGlobal(float x, float y) +{ + CGPoint point; + SDL_Mouse *mouse = SDL_GetMouse(); + if (mouse->focus) { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)mouse->focus->internal; + if ([data.listener isMovingOrFocusClickPending]) { + DLog("Postponing warp, window being moved or focused."); + [data.listener setPendingMoveX:x Y:y]; + return true; + } + } + point = CGPointMake(x, y); + + Cocoa_HandleMouseWarp(point.x, point.y); + + CGWarpMouseCursorPosition(point); + + /* CGWarpMouse causes a short delay by default, which is preventable by + * Calling this directly after. CGSetLocalEventsSuppressionInterval can also + * prevent it, but it's deprecated as macOS 10.6. + */ + if (!mouse->relative_mode) { + CGAssociateMouseAndMouseCursorPosition(YES); + } + + /* CGWarpMouseCursorPosition doesn't generate a window event, unlike our + * other implementations' APIs. Send what's appropriate. + */ + if (!mouse->relative_mode) { + SDL_Window *win = SDL_FindWindowAtPoint(x, y); + SDL_SetMouseFocus(win); + if (win) { + SDL_assert(win == mouse->focus); + SDL_SendMouseMotion(0, win, SDL_GLOBAL_MOUSE_ID, false, x - win->x, y - win->y); + } + } + + return true; +} + +static bool Cocoa_WarpMouse(SDL_Window *window, float x, float y) +{ + return Cocoa_WarpMouseGlobal(window->x + x, window->y + y); +} + +static bool Cocoa_SetRelativeMouseMode(bool enabled) +{ + CGError result; + + if (enabled) { + SDL_Window *window = SDL_GetKeyboardFocus(); + if (window) { + /* We will re-apply the relative mode when the window finishes being moved, + * if it is being moved right now. + */ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + if ([data.listener isMovingOrFocusClickPending]) { + return true; + } + + // 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. + const CGPoint point = CGPointMake((float)(window->x + (window->w / 2)), (float)(window->y + (window->h / 2))); + Cocoa_HandleMouseWarp(point.x, point.y); + CGWarpMouseCursorPosition(point); + } + DLog("Turning on."); + result = CGAssociateMouseAndMouseCursorPosition(NO); + } else { + DLog("Turning off."); + result = CGAssociateMouseAndMouseCursorPosition(YES); + } + if (result != kCGErrorSuccess) { + return SDL_SetError("CGAssociateMouseAndMouseCursorPosition() failed"); + } + + /* The hide/unhide calls are redundant most of the time, but they fix + * https://bugzilla.libsdl.org/show_bug.cgi?id=2550 + */ + if (enabled) { + [NSCursor hide]; + } else { + [NSCursor unhide]; + } + return true; +} + +static bool Cocoa_CaptureMouse(SDL_Window *window) +{ + /* our Cocoa event code already tracks the mouse outside the window, + so all we have to do here is say "okay" and do what we always do. */ + return true; +} + +static SDL_MouseButtonFlags Cocoa_GetGlobalMouseState(float *x, float *y) +{ + const NSUInteger cocoaButtons = [NSEvent pressedMouseButtons]; + const NSPoint cocoaLocation = [NSEvent mouseLocation]; + SDL_MouseButtonFlags result = 0; + + *x = cocoaLocation.x; + *y = (CGDisplayPixelsHigh(kCGDirectMainDisplay) - cocoaLocation.y); + + result |= (cocoaButtons & (1 << 0)) ? SDL_BUTTON_LMASK : 0; + result |= (cocoaButtons & (1 << 1)) ? SDL_BUTTON_RMASK : 0; + result |= (cocoaButtons & (1 << 2)) ? SDL_BUTTON_MMASK : 0; + result |= (cocoaButtons & (1 << 3)) ? SDL_BUTTON_X1MASK : 0; + result |= (cocoaButtons & (1 << 4)) ? SDL_BUTTON_X2MASK : 0; + + return result; +} + +bool Cocoa_InitMouse(SDL_VideoDevice *_this) +{ + NSPoint location; + SDL_Mouse *mouse = SDL_GetMouse(); + SDL_MouseData *internal = (SDL_MouseData *)SDL_calloc(1, sizeof(SDL_MouseData)); + if (internal == NULL) { + return false; + } + + mouse->internal = internal; + mouse->CreateCursor = Cocoa_CreateCursor; + mouse->CreateSystemCursor = Cocoa_CreateSystemCursor; + mouse->ShowCursor = Cocoa_ShowCursor; + mouse->FreeCursor = Cocoa_FreeCursor; + mouse->WarpMouse = Cocoa_WarpMouse; + mouse->WarpMouseGlobal = Cocoa_WarpMouseGlobal; + mouse->SetRelativeMouseMode = Cocoa_SetRelativeMouseMode; + mouse->CaptureMouse = Cocoa_CaptureMouse; + mouse->GetGlobalMouseState = Cocoa_GetGlobalMouseState; + + SDL_SetDefaultCursor(Cocoa_CreateDefaultCursor()); + + location = [NSEvent mouseLocation]; + internal->lastMoveX = location.x; + internal->lastMoveY = location.y; + return true; +} + +static void Cocoa_HandleTitleButtonEvent(SDL_VideoDevice *_this, NSEvent *event) +{ + SDL_Window *window; + NSWindow *nswindow = [event window]; + + /* You might land in this function before SDL_Init if showing a message box. + Don't dereference a NULL pointer if that happens. */ + if (_this == NULL) { + return; + } + + for (window = _this->windows; window; window = window->next) { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + if (data && data.nswindow == nswindow) { + switch ([event type]) { + case NSEventTypeLeftMouseDown: + case NSEventTypeRightMouseDown: + case NSEventTypeOtherMouseDown: + [data.listener setFocusClickPending:[event buttonNumber]]; + break; + case NSEventTypeLeftMouseUp: + case NSEventTypeRightMouseUp: + case NSEventTypeOtherMouseUp: + [data.listener clearFocusClickPending:[event buttonNumber]]; + break; + default: + break; + } + break; + } + } +} + +static NSWindow *Cocoa_MouseFocus; + +NSWindow *Cocoa_GetMouseFocus() +{ + return Cocoa_MouseFocus; +} + +void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event) +{ + SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID; + SDL_Mouse *mouse; + SDL_MouseData *data; + NSPoint location; + CGFloat lastMoveX, lastMoveY; + float deltaX, deltaY; + bool seenWarp; + + // All events except NSEventTypeMouseExited can only happen if the window + // has mouse focus, so we'll always set the focus even if we happen to miss + // NSEventTypeMouseEntered, which apparently happens if the window is + // created under the mouse on macOS 12.7 + NSEventType event_type = [event type]; + if (event_type == NSEventTypeMouseExited) { + Cocoa_MouseFocus = NULL; + } else { + Cocoa_MouseFocus = [event window]; + } + + switch (event_type) { + case NSEventTypeMouseEntered: + case NSEventTypeMouseExited: + // Focus is handled above + return; + + case NSEventTypeMouseMoved: + case NSEventTypeLeftMouseDragged: + case NSEventTypeRightMouseDragged: + case NSEventTypeOtherMouseDragged: + break; + + case NSEventTypeLeftMouseDown: + case NSEventTypeLeftMouseUp: + case NSEventTypeRightMouseDown: + case NSEventTypeRightMouseUp: + case NSEventTypeOtherMouseDown: + case NSEventTypeOtherMouseUp: + if ([event window]) { + NSRect windowRect = [[[event window] contentView] frame]; + if (!NSMouseInRect([event locationInWindow], windowRect, NO)) { + Cocoa_HandleTitleButtonEvent(_this, event); + return; + } + } + return; + + default: + // Ignore any other events. + return; + } + + mouse = SDL_GetMouse(); + data = (SDL_MouseData *)mouse->internal; + if (!data) { + return; // can happen when returning from fullscreen Space on shutdown + } + + seenWarp = data->seenWarp; + data->seenWarp = NO; + + location = [NSEvent mouseLocation]; + lastMoveX = data->lastMoveX; + lastMoveY = data->lastMoveY; + data->lastMoveX = location.x; + data->lastMoveY = location.y; + DLog("Last seen mouse: (%g, %g)", location.x, location.y); + + // Non-relative movement is handled in -[SDL3Cocoa_WindowListener mouseMoved:] + if (!mouse->relative_mode) { + return; + } + + // Ignore events that aren't inside the client area (i.e. title bar.) + if ([event window]) { + NSRect windowRect = [[[event window] contentView] frame]; + if (!NSMouseInRect([event locationInWindow], windowRect, NO)) { + return; + } + } + + deltaX = [event deltaX]; + deltaY = [event deltaY]; + + if (seenWarp) { + deltaX += (lastMoveX - data->lastWarpX); + deltaY += ((CGDisplayPixelsHigh(kCGDirectMainDisplay) - lastMoveY) - data->lastWarpY); + + DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], [event deltaY], deltaX, deltaY); + } + + SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]), mouse->focus, mouseID, true, deltaX, deltaY); +} + +void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event) +{ + SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID; + SDL_MouseWheelDirection direction; + CGFloat x, y; + + x = -[event deltaX]; + y = [event deltaY]; + direction = SDL_MOUSEWHEEL_NORMAL; + + if ([event isDirectionInvertedFromDevice] == YES) { + direction = SDL_MOUSEWHEEL_FLIPPED; + } + + /* For discrete scroll events from conventional mice, always send a full tick. + For continuous scroll events from trackpads, send fractional deltas for smoother scrolling. */ + if (![event hasPreciseScrollingDeltas]) { + if (x > 0) { + x = SDL_ceil(x); + } else if (x < 0) { + x = SDL_floor(x); + } + if (y > 0) { + y = SDL_ceil(y); + } else if (y < 0) { + y = SDL_floor(y); + } + } + + SDL_SendMouseWheel(Cocoa_GetEventTimestamp([event timestamp]), window, mouseID, x, y, direction); +} + +void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y) +{ + /* This makes Cocoa_HandleMouseEvent ignore the delta caused by the warp, + * since it gets included in the next movement event. + */ + SDL_MouseData *data = (SDL_MouseData *)SDL_GetMouse()->internal; + data->lastWarpX = x; + data->lastWarpY = y; + data->seenWarp = true; + + DLog("(%g, %g)", x, y); +} + +void Cocoa_QuitMouse(SDL_VideoDevice *_this) +{ + SDL_Mouse *mouse = SDL_GetMouse(); + if (mouse) { + if (mouse->internal) { + SDL_free(mouse->internal); + mouse->internal = NULL; + } + } +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoaopengl_h_ +#define SDL_cocoaopengl_h_ + +#ifdef SDL_VIDEO_OPENGL_CGL + +#import +#import + +// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it. +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + +struct SDL_GLDriverData +{ + int initialized; +}; + +@interface SDL3OpenGLContext : NSOpenGLContext +{ + SDL_AtomicInt dirty; + SDL_Window *window; + CVDisplayLinkRef displayLink; + @public + SDL_Mutex *swapIntervalMutex; + @public + SDL_Condition *swapIntervalCond; + @public + SDL_AtomicInt swapIntervalSetting; + @public + SDL_AtomicInt swapIntervalsPassed; +} + +- (id)initWithFormat:(NSOpenGLPixelFormat *)format + shareContext:(NSOpenGLContext *)share; +- (void)scheduleUpdate; +- (void)updateIfNeeded; +- (void)movedToNewScreen; +- (void)setWindow:(SDL_Window *)window; +- (SDL_Window *)window; +- (void)explicitUpdate; +- (void)cleanup; + +@property(retain, nonatomic) NSOpenGLPixelFormat *openglPixelFormat; // macOS 10.10 has -[NSOpenGLContext pixelFormat] but this handles older OS releases. + +@end + +// OpenGL functions +extern bool Cocoa_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path); +extern SDL_FunctionPointer Cocoa_GL_GetProcAddress(SDL_VideoDevice *_this, const char *proc); +extern void Cocoa_GL_UnloadLibrary(SDL_VideoDevice *_this); +extern SDL_GLContext Cocoa_GL_CreateContext(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_GL_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context); +extern bool Cocoa_GL_SetSwapInterval(SDL_VideoDevice *_this, int interval); +extern bool Cocoa_GL_GetSwapInterval(SDL_VideoDevice *_this, int *interval); +extern bool Cocoa_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_GL_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context); + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // SDL_VIDEO_OPENGL_CGL + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +// NSOpenGL implementation of SDL OpenGL support + +#ifdef SDL_VIDEO_OPENGL_CGL +#include "SDL_cocoavideo.h" +#include "SDL_cocoaopengl.h" +#include "SDL_cocoaopengles.h" + +#include +#include +#include + +#include +#include "../../SDL_hints_c.h" + +#define DEFAULT_OPENGL "/System/Library/Frameworks/OpenGL.framework/Libraries/libGL.dylib" + +// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it. +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + +// _Nullable is available starting Xcode 7 +#ifdef __has_feature +#if __has_feature(nullability) +#define HAS_FEATURE_NULLABLE +#endif +#endif +#ifndef HAS_FEATURE_NULLABLE +#define _Nullable +#endif + +static bool SDL_opengl_async_dispatch = false; + +static void SDLCALL SDL_OpenGLAsyncDispatchChanged(void *userdata, const char *name, const char *oldValue, const char *hint) +{ + SDL_opengl_async_dispatch = SDL_GetStringBoolean(hint, false); +} + +static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) +{ + SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)displayLinkContext; + + // printf("DISPLAY LINK! %u\n", (unsigned int) SDL_GetTicks()); + const int setting = SDL_GetAtomicInt(&nscontext->swapIntervalSetting); + if (setting != 0) { // nothing to do if vsync is disabled, don't even lock + SDL_LockMutex(nscontext->swapIntervalMutex); + SDL_AddAtomicInt(&nscontext->swapIntervalsPassed, 1); + SDL_SignalCondition(nscontext->swapIntervalCond); + SDL_UnlockMutex(nscontext->swapIntervalMutex); + } + + return kCVReturnSuccess; +} + +@implementation SDL3OpenGLContext : NSOpenGLContext + +- (id)initWithFormat:(NSOpenGLPixelFormat *)format + shareContext:(NSOpenGLContext *)share +{ + self = [super initWithFormat:format shareContext:share]; + if (self) { + self.openglPixelFormat = format; + SDL_SetAtomicInt(&self->dirty, 0); + self->window = NULL; + SDL_SetAtomicInt(&self->swapIntervalSetting, 0); + SDL_SetAtomicInt(&self->swapIntervalsPassed, 0); + self->swapIntervalCond = SDL_CreateCondition(); + self->swapIntervalMutex = SDL_CreateMutex(); + if (!self->swapIntervalCond || !self->swapIntervalMutex) { + return nil; + } + + // !!! FIXME: check return values. + CVDisplayLinkCreateWithActiveCGDisplays(&self->displayLink); + CVDisplayLinkSetOutputCallback(self->displayLink, &DisplayLinkCallback, (__bridge void *_Nullable)self); + CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(self->displayLink, [self CGLContextObj], [format CGLPixelFormatObj]); + CVDisplayLinkStart(displayLink); + } + + SDL_AddHintCallback(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, SDL_OpenGLAsyncDispatchChanged, NULL); + return self; +} + +- (void)movedToNewScreen +{ + if (self->displayLink) { + CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(self->displayLink, [self CGLContextObj], [[self openglPixelFormat] CGLPixelFormatObj]); + } +} + +- (void)scheduleUpdate +{ + SDL_AddAtomicInt(&self->dirty, 1); +} + +// This should only be called on the thread on which a user is using the context. +- (void)updateIfNeeded +{ + const int value = SDL_SetAtomicInt(&self->dirty, 0); + if (value > 0) { + // We call the real underlying update here, since -[SDL3OpenGLContext update] just calls us. + [self explicitUpdate]; + } +} + +// This should only be called on the thread on which a user is using the context. +- (void)update +{ + // This ensures that regular 'update' calls clear the atomic dirty flag. + [self scheduleUpdate]; + [self updateIfNeeded]; +} + +// Updates the drawable for the contexts and manages related state. +- (void)setWindow:(SDL_Window *)newWindow +{ + if (self->window) { + SDL_CocoaWindowData *oldwindowdata = (__bridge SDL_CocoaWindowData *)self->window->internal; + + // Make sure to remove us from the old window's context list, or we'll get scheduled updates from it too. + NSMutableArray *contexts = oldwindowdata.nscontexts; + @synchronized(contexts) { + [contexts removeObject:self]; + } + } + + self->window = newWindow; + + if (newWindow) { + SDL_CocoaWindowData *windowdata = (__bridge SDL_CocoaWindowData *)newWindow->internal; + NSView *contentview = windowdata.sdlContentView; + + // Now sign up for scheduled updates for the new window. + NSMutableArray *contexts = windowdata.nscontexts; + @synchronized(contexts) { + [contexts addObject:self]; + } + + if ([self view] != contentview) { + if ([NSThread isMainThread]) { + [self setView:contentview]; + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self setView:contentview]; + }); + } + if (self == [NSOpenGLContext currentContext]) { + [self explicitUpdate]; + } else { + [self scheduleUpdate]; + } + } + } else { + if ([NSThread isMainThread]) { + [self setView:nil]; + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ [self setView:nil]; }); + } + } +} + +- (SDL_Window *)window +{ + return self->window; +} + +- (void)explicitUpdate +{ + if ([NSThread isMainThread]) { + [super update]; + } else { + if (SDL_opengl_async_dispatch) { + dispatch_async(dispatch_get_main_queue(), ^{ + [super update]; + }); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + [super update]; + }); + } + } +} + +- (void)cleanup +{ + [self setWindow:NULL]; + + SDL_RemoveHintCallback(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, SDL_OpenGLAsyncDispatchChanged, NULL); + if (self->displayLink) { + CVDisplayLinkRelease(self->displayLink); + self->displayLink = nil; + } + if (self->swapIntervalCond) { + SDL_DestroyCondition(self->swapIntervalCond); + self->swapIntervalCond = NULL; + } + if (self->swapIntervalMutex) { + SDL_DestroyMutex(self->swapIntervalMutex); + self->swapIntervalMutex = NULL; + } +} + +@end + +bool Cocoa_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + // Load the OpenGL library + if (path == NULL) { + path = SDL_GetHint(SDL_HINT_OPENGL_LIBRARY); + } + if (path == NULL) { + path = DEFAULT_OPENGL; + } + _this->gl_config.dll_handle = SDL_LoadObject(path); + if (!_this->gl_config.dll_handle) { + return false; + } + SDL_strlcpy(_this->gl_config.driver_path, path, + SDL_arraysize(_this->gl_config.driver_path)); + return true; +} + +SDL_FunctionPointer Cocoa_GL_GetProcAddress(SDL_VideoDevice *_this, const char *proc) +{ + return SDL_LoadFunction(_this->gl_config.dll_handle, proc); +} + +void Cocoa_GL_UnloadLibrary(SDL_VideoDevice *_this) +{ + SDL_UnloadObject(_this->gl_config.dll_handle); + _this->gl_config.dll_handle = NULL; +} + +SDL_GLContext Cocoa_GL_CreateContext(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window); + SDL_DisplayData *displaydata = (SDL_DisplayData *)display->internal; + NSOpenGLPixelFormatAttribute attr[32]; + NSOpenGLPixelFormat *fmt; + SDL3OpenGLContext *context; + SDL_GLContext sdlcontext; + NSOpenGLContext *share_context = nil; + int i = 0; + const char *glversion; + int glversion_major; + int glversion_minor; + NSOpenGLPixelFormatAttribute profile; + int interval; + int opaque; + + if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_ES) { +#ifdef SDL_VIDEO_OPENGL_EGL + // Switch to EGL based functions + Cocoa_GL_UnloadLibrary(_this); + _this->GL_LoadLibrary = Cocoa_GLES_LoadLibrary; + _this->GL_GetProcAddress = Cocoa_GLES_GetProcAddress; + _this->GL_UnloadLibrary = Cocoa_GLES_UnloadLibrary; + _this->GL_CreateContext = Cocoa_GLES_CreateContext; + _this->GL_MakeCurrent = Cocoa_GLES_MakeCurrent; + _this->GL_SetSwapInterval = Cocoa_GLES_SetSwapInterval; + _this->GL_GetSwapInterval = Cocoa_GLES_GetSwapInterval; + _this->GL_SwapWindow = Cocoa_GLES_SwapWindow; + _this->GL_DestroyContext = Cocoa_GLES_DestroyContext; + + if (!Cocoa_GLES_LoadLibrary(_this, NULL)) { + return NULL; + } + return Cocoa_GLES_CreateContext(_this, window); +#else + SDL_SetError("SDL not configured with EGL support"); + return NULL; +#endif + } + + attr[i++] = NSOpenGLPFAAllowOfflineRenderers; + + profile = NSOpenGLProfileVersionLegacy; + if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_CORE) { + profile = NSOpenGLProfileVersion3_2Core; + } + attr[i++] = NSOpenGLPFAOpenGLProfile; + attr[i++] = profile; + + attr[i++] = NSOpenGLPFAColorSize; + attr[i++] = SDL_BYTESPERPIXEL(display->current_mode->format) * 8; + + attr[i++] = NSOpenGLPFADepthSize; + attr[i++] = _this->gl_config.depth_size; + + if (_this->gl_config.double_buffer) { + attr[i++] = NSOpenGLPFADoubleBuffer; + } + + if (_this->gl_config.stereo) { + attr[i++] = NSOpenGLPFAStereo; + } + + if (_this->gl_config.stencil_size) { + attr[i++] = NSOpenGLPFAStencilSize; + attr[i++] = _this->gl_config.stencil_size; + } + + if ((_this->gl_config.accum_red_size + + _this->gl_config.accum_green_size + + _this->gl_config.accum_blue_size + + _this->gl_config.accum_alpha_size) > 0) { + attr[i++] = NSOpenGLPFAAccumSize; + 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; + } + + if (_this->gl_config.multisamplebuffers) { + attr[i++] = NSOpenGLPFASampleBuffers; + attr[i++] = _this->gl_config.multisamplebuffers; + } + + if (_this->gl_config.multisamplesamples) { + attr[i++] = NSOpenGLPFASamples; + attr[i++] = _this->gl_config.multisamplesamples; + attr[i++] = NSOpenGLPFANoRecovery; + } + if (_this->gl_config.floatbuffers) { + attr[i++] = NSOpenGLPFAColorFloat; + } + + if (_this->gl_config.accelerated >= 0) { + if (_this->gl_config.accelerated) { + attr[i++] = NSOpenGLPFAAccelerated; + } else { + attr[i++] = NSOpenGLPFARendererID; + attr[i++] = kCGLRendererGenericFloatID; + } + } + + attr[i++] = NSOpenGLPFAScreenMask; + attr[i++] = CGDisplayIDToOpenGLDisplayMask(displaydata->display); + attr[i] = 0; + + fmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + if (fmt == nil) { + SDL_SetError("Failed creating OpenGL pixel format"); + return NULL; + } + + if (_this->gl_config.share_with_current_context) { + share_context = (__bridge NSOpenGLContext *)SDL_GL_GetCurrentContext(); + } + + context = [[SDL3OpenGLContext alloc] initWithFormat:fmt shareContext:share_context]; + + if (context == nil) { + SDL_SetError("Failed creating OpenGL context"); + return NULL; + } + + sdlcontext = (SDL_GLContext)CFBridgingRetain(context); + + // vsync is handled separately by synchronizing with a display link. + interval = 0; + [context setValues:&interval forParameter:NSOpenGLCPSwapInterval]; + + opaque = (window->flags & SDL_WINDOW_TRANSPARENT) ? 0 : 1; + [context setValues:&opaque forParameter:NSOpenGLCPSurfaceOpacity]; + + if (!Cocoa_GL_MakeCurrent(_this, window, sdlcontext)) { + SDL_GL_DestroyContext(sdlcontext); + SDL_SetError("Failed making OpenGL context current"); + return NULL; + } + + if (_this->gl_config.major_version < 3 && + _this->gl_config.profile_mask == 0 && + _this->gl_config.flags == 0) { + // This is a legacy profile, so to match other backends, we're done. + } else { + const GLubyte *(APIENTRY * glGetStringFunc)(GLenum); + + glGetStringFunc = (const GLubyte *(APIENTRY *)(GLenum))SDL_GL_GetProcAddress("glGetString"); + if (!glGetStringFunc) { + SDL_GL_DestroyContext(sdlcontext); + SDL_SetError("Failed getting OpenGL glGetString entry point"); + return NULL; + } + + glversion = (const char *)glGetStringFunc(GL_VERSION); + if (glversion == NULL) { + SDL_GL_DestroyContext(sdlcontext); + SDL_SetError("Failed getting OpenGL context version"); + return NULL; + } + + if (SDL_sscanf(glversion, "%d.%d", &glversion_major, &glversion_minor) != 2) { + SDL_GL_DestroyContext(sdlcontext); + SDL_SetError("Failed parsing OpenGL context version"); + return NULL; + } + + if ((glversion_major < _this->gl_config.major_version) || + ((glversion_major == _this->gl_config.major_version) && (glversion_minor < _this->gl_config.minor_version))) { + SDL_GL_DestroyContext(sdlcontext); + SDL_SetError("Failed creating OpenGL context at version requested"); + return NULL; + } + + /* In the future we'll want to do this, but to match other platforms + we'll leave the OpenGL version the way it is for now + */ + // _this->gl_config.major_version = glversion_major; + // _this->gl_config.minor_version = glversion_minor; + } + return sdlcontext; + } +} + +bool Cocoa_GL_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context) +{ + @autoreleasepool { + if (context) { + SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)context; + if ([nscontext window] != window) { + [nscontext setWindow:window]; + [nscontext updateIfNeeded]; + } + [nscontext makeCurrentContext]; + } else { + [NSOpenGLContext clearCurrentContext]; + } + + return true; + } +} + +bool Cocoa_GL_SetSwapInterval(SDL_VideoDevice *_this, int interval) +{ + @autoreleasepool { + SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)SDL_GL_GetCurrentContext(); + bool result; + + if (nscontext == nil) { + result = SDL_SetError("No current OpenGL context"); + } else { + SDL_LockMutex(nscontext->swapIntervalMutex); + SDL_SetAtomicInt(&nscontext->swapIntervalsPassed, 0); + SDL_SetAtomicInt(&nscontext->swapIntervalSetting, interval); + SDL_UnlockMutex(nscontext->swapIntervalMutex); + result = true; + } + + return result; + } +} + +bool Cocoa_GL_GetSwapInterval(SDL_VideoDevice *_this, int *interval) +{ + @autoreleasepool { + SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)SDL_GL_GetCurrentContext(); + if (nscontext) { + *interval = SDL_GetAtomicInt(&nscontext->swapIntervalSetting); + return true; + } else { + return SDL_SetError("no OpenGL context"); + } + } +} + +bool Cocoa_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)SDL_GL_GetCurrentContext(); + SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal; + const int setting = SDL_GetAtomicInt(&nscontext->swapIntervalSetting); + + if (setting == 0) { + // nothing to do if vsync is disabled, don't even lock + } else if (setting < 0) { // late swap tearing + SDL_LockMutex(nscontext->swapIntervalMutex); + while (SDL_GetAtomicInt(&nscontext->swapIntervalsPassed) == 0) { + SDL_WaitCondition(nscontext->swapIntervalCond, nscontext->swapIntervalMutex); + } + SDL_SetAtomicInt(&nscontext->swapIntervalsPassed, 0); + SDL_UnlockMutex(nscontext->swapIntervalMutex); + } else { + SDL_LockMutex(nscontext->swapIntervalMutex); + do { // always wait here so we know we just hit a swap interval. + SDL_WaitCondition(nscontext->swapIntervalCond, nscontext->swapIntervalMutex); + } while ((SDL_GetAtomicInt(&nscontext->swapIntervalsPassed) % setting) != 0); + SDL_SetAtomicInt(&nscontext->swapIntervalsPassed, 0); + SDL_UnlockMutex(nscontext->swapIntervalMutex); + } + + // { 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); } + + /* on 10.14 ("Mojave") and later, this deadlocks if two contexts in two + threads try to swap at the same time, so put a mutex around it. */ + SDL_LockMutex(videodata.swaplock); + [nscontext flushBuffer]; + [nscontext updateIfNeeded]; + SDL_UnlockMutex(videodata.swaplock); + return true; + } +} + +static void DispatchedDestroyContext(SDL_GLContext context) +{ + @autoreleasepool { + SDL3OpenGLContext *nscontext = (__bridge SDL3OpenGLContext *)context; + [nscontext cleanup]; + CFRelease(context); + } +} + +bool Cocoa_GL_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context) +{ + if ([NSThread isMainThread]) { + DispatchedDestroyContext(context); + } else { + if (SDL_opengl_async_dispatch) { + dispatch_async(dispatch_get_main_queue(), ^{ + DispatchedDestroyContext(context); + }); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + DispatchedDestroyContext(context); + }); + } + } + + return true; +} + +// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it. +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoaopengles_h_ +#define SDL_cocoaopengles_h_ + +#ifdef SDL_VIDEO_OPENGL_EGL + +#include "../SDL_sysvideo.h" +#include "../SDL_egl_c.h" + +// OpenGLES functions +#define Cocoa_GLES_GetAttribute SDL_EGL_GetAttribute +#define Cocoa_GLES_GetProcAddress SDL_EGL_GetProcAddressInternal +#define Cocoa_GLES_UnloadLibrary SDL_EGL_UnloadLibrary +#define Cocoa_GLES_GetSwapInterval SDL_EGL_GetSwapInterval +#define Cocoa_GLES_SetSwapInterval SDL_EGL_SetSwapInterval + +extern bool Cocoa_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path); +extern SDL_GLContext Cocoa_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context); +extern bool Cocoa_GLES_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context); +extern bool Cocoa_GLES_SetupWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern SDL_EGLSurface Cocoa_GLES_GetEGLSurface(SDL_VideoDevice *_this, SDL_Window *window); + +#endif // SDL_VIDEO_OPENGL_EGL + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#if defined(SDL_VIDEO_DRIVER_COCOA) && defined(SDL_VIDEO_OPENGL_EGL) + +#include "SDL_cocoavideo.h" +#include "SDL_cocoaopengles.h" +#include "SDL_cocoaopengl.h" + +// EGL implementation of SDL OpenGL support + +bool Cocoa_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + // If the profile requested is not GL ES, switch over to WIN_GL functions + if (_this->gl_config.profile_mask != SDL_GL_CONTEXT_PROFILE_ES) { +#ifdef SDL_VIDEO_OPENGL_CGL + Cocoa_GLES_UnloadLibrary(_this); + _this->GL_LoadLibrary = Cocoa_GL_LoadLibrary; + _this->GL_GetProcAddress = Cocoa_GL_GetProcAddress; + _this->GL_UnloadLibrary = Cocoa_GL_UnloadLibrary; + _this->GL_CreateContext = Cocoa_GL_CreateContext; + _this->GL_MakeCurrent = Cocoa_GL_MakeCurrent; + _this->GL_SetSwapInterval = Cocoa_GL_SetSwapInterval; + _this->GL_GetSwapInterval = Cocoa_GL_GetSwapInterval; + _this->GL_SwapWindow = Cocoa_GL_SwapWindow; + _this->GL_DestroyContext = Cocoa_GL_DestroyContext; + _this->GL_GetEGLSurface = NULL; + return Cocoa_GL_LoadLibrary(_this, path); +#else + return SDL_SetError("SDL not configured with OpenGL/CGL support"); +#endif + } + + if (_this->egl_data == NULL) { + return SDL_EGL_LoadLibrary(_this, NULL, EGL_DEFAULT_DISPLAY, _this->gl_config.egl_platform); + } + + return true; +} + +SDL_GLContext Cocoa_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_GLContext context; + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + +#ifdef SDL_VIDEO_OPENGL_CGL + if (_this->gl_config.profile_mask != SDL_GL_CONTEXT_PROFILE_ES) { + // Switch to CGL based functions + Cocoa_GLES_UnloadLibrary(_this); + _this->GL_LoadLibrary = Cocoa_GL_LoadLibrary; + _this->GL_GetProcAddress = Cocoa_GL_GetProcAddress; + _this->GL_UnloadLibrary = Cocoa_GL_UnloadLibrary; + _this->GL_CreateContext = Cocoa_GL_CreateContext; + _this->GL_MakeCurrent = Cocoa_GL_MakeCurrent; + _this->GL_SetSwapInterval = Cocoa_GL_SetSwapInterval; + _this->GL_GetSwapInterval = Cocoa_GL_GetSwapInterval; + _this->GL_SwapWindow = Cocoa_GL_SwapWindow; + _this->GL_DestroyContext = Cocoa_GL_DestroyContext; + _this->GL_GetEGLSurface = NULL; + + if (!Cocoa_GL_LoadLibrary(_this, NULL)) { + return NULL; + } + + return Cocoa_GL_CreateContext(_this, window); + } +#endif + + context = SDL_EGL_CreateContext(_this, data.egl_surface); + return context; + } +} + +bool Cocoa_GLES_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context) +{ + @autoreleasepool { + SDL_EGL_DestroyContext(_this, context); + } + return true; +} + +bool Cocoa_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + return SDL_EGL_SwapBuffers(_this, ((__bridge SDL_CocoaWindowData *)window->internal).egl_surface); + } +} + +bool Cocoa_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context) +{ + @autoreleasepool { + return SDL_EGL_MakeCurrent(_this, window ? ((__bridge SDL_CocoaWindowData *)window->internal).egl_surface : EGL_NO_SURFACE, context); + } +} + +bool Cocoa_GLES_SetupWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + NSView *v; + // The current context is lost in here; save it and reset it. + SDL_CocoaWindowData *windowdata = (__bridge SDL_CocoaWindowData *)window->internal; + SDL_Window *current_win = SDL_GL_GetCurrentWindow(); + SDL_GLContext current_ctx = SDL_GL_GetCurrentContext(); + + if (_this->egl_data == NULL) { +// !!! FIXME: commenting out this assertion is (I think) incorrect; figure out why driver_loaded is wrong for ANGLE instead. --ryan. +#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. + SDL_assert(!_this->gl_config.driver_loaded); +#endif + if (!SDL_EGL_LoadLibrary(_this, NULL, EGL_DEFAULT_DISPLAY, _this->gl_config.egl_platform)) { + SDL_EGL_UnloadLibrary(_this); + return false; + } + _this->gl_config.driver_loaded = 1; + } + + // Create the GLES window surface + v = windowdata.nswindow.contentView; + windowdata.egl_surface = SDL_EGL_CreateSurface(_this, window, (__bridge NativeWindowType)[v layer]); + + if (windowdata.egl_surface == EGL_NO_SURFACE) { + return SDL_SetError("Could not create GLES window surface"); + } + + return Cocoa_GLES_MakeCurrent(_this, current_win, current_ctx); + } +} + +SDL_EGLSurface Cocoa_GLES_GetEGLSurface(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + return ((__bridge SDL_CocoaWindowData *)window->internal).egl_surface; + } +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoapen_h_ +#define SDL_cocoapenm_h_ + +#include "SDL_cocoavideo.h" + +extern bool Cocoa_InitPen(SDL_VideoDevice *_this); +extern bool Cocoa_HandlePenEvent(SDL_CocoaWindowData *_data, NSEvent *event); // return false if we didn't handle this event. +extern void Cocoa_QuitPen(SDL_VideoDevice *_this); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoapen.h" +#include "SDL_cocoavideo.h" + +#include "../../events/SDL_pen_c.h" + +bool Cocoa_InitPen(SDL_VideoDevice *_this) +{ + return true; +} + +typedef struct Cocoa_PenHandle +{ + NSUInteger deviceid; + NSUInteger toolid; + SDL_PenID pen; + bool is_eraser; +} Cocoa_PenHandle; + +typedef struct FindPenByDeviceAndToolIDData +{ + NSUInteger deviceid; + NSUInteger toolid; + void *handle; +} FindPenByDeviceAndToolIDData; + +static bool FindPenByDeviceAndToolID(void *handle, void *userdata) +{ + const Cocoa_PenHandle *cocoa_handle = (const Cocoa_PenHandle *) handle; + FindPenByDeviceAndToolIDData *data = (FindPenByDeviceAndToolIDData *) userdata; + + if (cocoa_handle->deviceid != data->deviceid) { + return false; + } else if (cocoa_handle->toolid != data->toolid) { + return false; + } + data->handle = handle; + return true; +} + +static Cocoa_PenHandle *Cocoa_FindPenByDeviceID(NSUInteger deviceid, NSUInteger toolid) +{ + FindPenByDeviceAndToolIDData data; + data.deviceid = deviceid; + data.toolid = toolid; + data.handle = NULL; + SDL_FindPenByCallback(FindPenByDeviceAndToolID, &data); + return (Cocoa_PenHandle *) data.handle; +} + +static void Cocoa_HandlePenProximityEvent(SDL_CocoaWindowData *_data, NSEvent *event) +{ + const NSUInteger devid = [event deviceID]; + const NSUInteger toolid = [event pointingDeviceID]; + + if (event.enteringProximity) { // new pen coming! + const NSPointingDeviceType devtype = [event pointingDeviceType]; + const bool is_eraser = (devtype == NSPointingDeviceTypeEraser); + const bool is_pen = (devtype == NSPointingDeviceTypePen); + if (!is_eraser && !is_pen) { + return; // we ignore other things, which hopefully is right. + } + + Cocoa_PenHandle *handle = (Cocoa_PenHandle *) SDL_calloc(1, sizeof (*handle)); + if (!handle) { + return; // oh well. + } + + // Cocoa offers almost none of this information as specifics, but can without warning offer any of these specific things. + SDL_PenInfo peninfo; + SDL_zero(peninfo); + 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); + peninfo.max_tilt = 90.0f; + peninfo.num_buttons = 2; + peninfo.subtype = is_eraser ? SDL_PEN_TYPE_ERASER : SDL_PEN_TYPE_PEN; + + handle->deviceid = devid; + handle->toolid = toolid; + handle->is_eraser = is_eraser; + handle->pen = SDL_AddPenDevice(Cocoa_GetEventTimestamp([event timestamp]), NULL, &peninfo, handle); + if (!handle->pen) { + SDL_free(handle); // oh well. + } + } else { // old pen leaving! + Cocoa_PenHandle *handle = Cocoa_FindPenByDeviceID(devid, toolid); + if (handle) { + SDL_RemovePenDevice(Cocoa_GetEventTimestamp([event timestamp]), handle->pen); + SDL_free(handle); + } + } +} + +static void Cocoa_HandlePenPointEvent(SDL_CocoaWindowData *_data, NSEvent *event) +{ + const Uint64 timestamp = Cocoa_GetEventTimestamp([event timestamp]); + Cocoa_PenHandle *handle = Cocoa_FindPenByDeviceID([event deviceID], [event pointingDeviceID]); + if (!handle) { + return; + } + + const SDL_PenID pen = handle->pen; + const NSEventButtonMask buttons = [event buttonMask]; + const NSPoint tilt = [event tilt]; + const NSPoint point = [event locationInWindow]; + const bool is_touching = (buttons & NSEventButtonMaskPenTip) != 0; + SDL_Window *window = _data.window; + + SDL_SendPenTouch(timestamp, pen, window, handle->is_eraser, is_touching); + SDL_SendPenMotion(timestamp, pen, window, (float) point.x, (float) (window->h - point.y)); + SDL_SendPenButton(timestamp, pen, window, 1, ((buttons & NSEventButtonMaskPenLowerSide) != 0)); + SDL_SendPenButton(timestamp, pen, window, 2, ((buttons & NSEventButtonMaskPenUpperSide) != 0)); + SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_PRESSURE, [event pressure]); + SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_ROTATION, [event rotation]); + SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_XTILT, ((float) tilt.x) * 90.0f); + SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_YTILT, ((float) -tilt.y) * 90.0f); + SDL_SendPenAxis(timestamp, pen, window, SDL_PEN_AXIS_TANGENTIAL_PRESSURE, event.tangentialPressure); +} + +bool Cocoa_HandlePenEvent(SDL_CocoaWindowData *_data, NSEvent *event) +{ + NSEventType type = [event type]; + + if ((type != NSEventTypeTabletPoint) && (type != NSEventTypeTabletProximity)) { + const NSEventSubtype subtype = [event subtype]; + if (subtype == NSEventSubtypeTabletPoint) { + type = NSEventTypeTabletPoint; + } else if (subtype == NSEventSubtypeTabletProximity) { + type = NSEventTypeTabletProximity; + } else { + return false; // not a tablet event. + } + } + + if (type == NSEventTypeTabletPoint) { + Cocoa_HandlePenPointEvent(_data, event); + } else if (type == NSEventTypeTabletProximity) { + Cocoa_HandlePenProximityEvent(_data, event); + } else { + return false; // not a tablet event. + } + + return true; +} + +static void Cocoa_FreePenHandle(SDL_PenID instance_id, void *handle, void *userdata) +{ + SDL_free(handle); +} + +void Cocoa_QuitPen(SDL_VideoDevice *_this) +{ + SDL_RemoveAllPenDevices(Cocoa_FreePenHandle, NULL); +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoashape_h_ +#define SDL_cocoashape_h_ + +extern bool Cocoa_UpdateWindowShape(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *shape); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoavideo.h" +#include "SDL_cocoashape.h" + + +bool Cocoa_UpdateWindowShape(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *shape) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + BOOL ignoresMouseEvents = NO; + + if (shape) { + SDL_FPoint point; + SDL_GetGlobalMouseState(&point.x, &point.y); + point.x -= window->x; + point.y -= window->y; + if (point.x >= 0.0f && point.x < window->w && + point.y >= 0.0f && point.y < window->h) { + int x = (int)SDL_roundf((point.x / (window->w - 1)) * (shape->w - 1)); + int y = (int)SDL_roundf((point.y / (window->h - 1)) * (shape->h - 1)); + Uint8 a; + + if (!SDL_ReadSurfacePixel(shape, x, y, NULL, NULL, NULL, &a) || a == SDL_ALPHA_TRANSPARENT) { + ignoresMouseEvents = YES; + } + } + } + data.nswindow.ignoresMouseEvents = ignoresMouseEvents; + return true; +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoavideo_h_ +#define SDL_cocoavideo_h_ + +#include + +#include +#include +#include + +#include "../SDL_sysvideo.h" + +#include "SDL_cocoaclipboard.h" +#include "SDL_cocoaevents.h" +#include "SDL_cocoakeyboard.h" +#include "SDL_cocoamodes.h" +#include "SDL_cocoamouse.h" +#include "SDL_cocoaopengl.h" +#include "SDL_cocoawindow.h" +#include "SDL_cocoapen.h" + +// Private display data + +@class SDL3TranslatorResponder; + +typedef enum +{ + OptionAsAltNone, + OptionAsAltOnlyLeft, + OptionAsAltOnlyRight, + OptionAsAltBoth, +} OptionAsAlt; + +@interface SDL_CocoaVideoData : NSObject +@property(nonatomic) int allow_spaces; +@property(nonatomic) int trackpad_is_touch_only; +@property(nonatomic) unsigned int modifierFlags; +@property(nonatomic) void *key_layout; +@property(nonatomic) SDL3TranslatorResponder *fieldEdit; +@property(nonatomic) NSInteger clipboard_count; +@property(nonatomic) IOPMAssertionID screensaver_assertion; +@property(nonatomic) SDL_Mutex *swaplock; +@property(nonatomic) OptionAsAlt option_as_alt; +@end + +// Utility functions +extern SDL_SystemTheme Cocoa_GetSystemTheme(void); +extern NSImage *Cocoa_CreateImage(SDL_Surface *surface); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#if !__has_feature(objc_arc) +#error SDL must be built with Objective-C ARC (automatic reference counting) enabled +#endif + +#include "SDL_cocoavideo.h" +#include "SDL_cocoavulkan.h" +#include "SDL_cocoametalview.h" +#include "SDL_cocoaopengles.h" +#include "SDL_cocoamessagebox.h" +#include "SDL_cocoashape.h" + +#include "../../events/SDL_keyboard_c.h" +#include "../../events/SDL_mouse_c.h" + +@implementation SDL_CocoaVideoData + +@end + +// Initialization/Query functions +static bool Cocoa_VideoInit(SDL_VideoDevice *_this); +static void Cocoa_VideoQuit(SDL_VideoDevice *_this); + +// Cocoa driver bootstrap functions + +static void Cocoa_DeleteDevice(SDL_VideoDevice *device) +{ + @autoreleasepool { + if (device->wakeup_lock) { + SDL_DestroyMutex(device->wakeup_lock); + } + CFBridgingRelease(device->internal); + SDL_free(device); + } +} + +static SDL_VideoDevice *Cocoa_CreateDevice(void) +{ + @autoreleasepool { + SDL_VideoDevice *device; + SDL_CocoaVideoData *data; + + if (![NSThread isMainThread]) { + return NULL; // this doesn't SDL_SetError() because SDL_VideoInit is just going to overwrite it. + } + + Cocoa_RegisterApp(); + + // Initialize all variables that we clean on shutdown + device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice)); + if (device) { + data = [[SDL_CocoaVideoData alloc] init]; + } else { + data = nil; + } + if (!data) { + SDL_free(device); + return NULL; + } + device->internal = (SDL_VideoData *)CFBridgingRetain(data); + device->wakeup_lock = SDL_CreateMutex(); + device->system_theme = Cocoa_GetSystemTheme(); + + // Set the function pointers + device->VideoInit = Cocoa_VideoInit; + device->VideoQuit = Cocoa_VideoQuit; + device->GetDisplayBounds = Cocoa_GetDisplayBounds; + device->GetDisplayUsableBounds = Cocoa_GetDisplayUsableBounds; + device->GetDisplayModes = Cocoa_GetDisplayModes; + device->SetDisplayMode = Cocoa_SetDisplayMode; + device->PumpEvents = Cocoa_PumpEvents; + device->WaitEventTimeout = Cocoa_WaitEventTimeout; + device->SendWakeupEvent = Cocoa_SendWakeupEvent; + device->SuspendScreenSaver = Cocoa_SuspendScreenSaver; + + device->CreateSDLWindow = Cocoa_CreateWindow; + device->SetWindowTitle = Cocoa_SetWindowTitle; + device->SetWindowIcon = Cocoa_SetWindowIcon; + device->SetWindowPosition = Cocoa_SetWindowPosition; + device->SetWindowSize = Cocoa_SetWindowSize; + device->SetWindowMinimumSize = Cocoa_SetWindowMinimumSize; + device->SetWindowMaximumSize = Cocoa_SetWindowMaximumSize; + device->SetWindowAspectRatio = Cocoa_SetWindowAspectRatio; + device->SetWindowOpacity = Cocoa_SetWindowOpacity; + device->GetWindowSizeInPixels = Cocoa_GetWindowSizeInPixels; + device->ShowWindow = Cocoa_ShowWindow; + device->HideWindow = Cocoa_HideWindow; + device->RaiseWindow = Cocoa_RaiseWindow; + device->MaximizeWindow = Cocoa_MaximizeWindow; + device->MinimizeWindow = Cocoa_MinimizeWindow; + device->RestoreWindow = Cocoa_RestoreWindow; + device->SetWindowBordered = Cocoa_SetWindowBordered; + device->SetWindowResizable = Cocoa_SetWindowResizable; + device->SetWindowAlwaysOnTop = Cocoa_SetWindowAlwaysOnTop; + device->SetWindowFullscreen = Cocoa_SetWindowFullscreen; + device->GetWindowICCProfile = Cocoa_GetWindowICCProfile; + device->GetDisplayForWindow = Cocoa_GetDisplayForWindow; + device->SetWindowMouseRect = Cocoa_SetWindowMouseRect; + device->SetWindowMouseGrab = Cocoa_SetWindowMouseGrab; + device->SetWindowKeyboardGrab = Cocoa_SetWindowKeyboardGrab; + device->DestroyWindow = Cocoa_DestroyWindow; + device->SetWindowHitTest = Cocoa_SetWindowHitTest; + device->AcceptDragAndDrop = Cocoa_AcceptDragAndDrop; + device->UpdateWindowShape = Cocoa_UpdateWindowShape; + device->FlashWindow = Cocoa_FlashWindow; + device->SetWindowFocusable = Cocoa_SetWindowFocusable; + device->SetWindowParent = Cocoa_SetWindowParent; + device->SetWindowModal = Cocoa_SetWindowModal; + device->SyncWindow = Cocoa_SyncWindow; + +#ifdef SDL_VIDEO_OPENGL_CGL + device->GL_LoadLibrary = Cocoa_GL_LoadLibrary; + device->GL_GetProcAddress = Cocoa_GL_GetProcAddress; + device->GL_UnloadLibrary = Cocoa_GL_UnloadLibrary; + device->GL_CreateContext = Cocoa_GL_CreateContext; + device->GL_MakeCurrent = Cocoa_GL_MakeCurrent; + device->GL_SetSwapInterval = Cocoa_GL_SetSwapInterval; + device->GL_GetSwapInterval = Cocoa_GL_GetSwapInterval; + device->GL_SwapWindow = Cocoa_GL_SwapWindow; + device->GL_DestroyContext = Cocoa_GL_DestroyContext; + device->GL_GetEGLSurface = NULL; +#endif +#ifdef SDL_VIDEO_OPENGL_EGL +#ifdef SDL_VIDEO_OPENGL_CGL + if (SDL_GetHintBoolean(SDL_HINT_VIDEO_FORCE_EGL, false)) { +#endif + device->GL_LoadLibrary = Cocoa_GLES_LoadLibrary; + device->GL_GetProcAddress = Cocoa_GLES_GetProcAddress; + device->GL_UnloadLibrary = Cocoa_GLES_UnloadLibrary; + device->GL_CreateContext = Cocoa_GLES_CreateContext; + device->GL_MakeCurrent = Cocoa_GLES_MakeCurrent; + device->GL_SetSwapInterval = Cocoa_GLES_SetSwapInterval; + device->GL_GetSwapInterval = Cocoa_GLES_GetSwapInterval; + device->GL_SwapWindow = Cocoa_GLES_SwapWindow; + device->GL_DestroyContext = Cocoa_GLES_DestroyContext; + device->GL_GetEGLSurface = Cocoa_GLES_GetEGLSurface; +#ifdef SDL_VIDEO_OPENGL_CGL + } +#endif +#endif + +#ifdef SDL_VIDEO_VULKAN + device->Vulkan_LoadLibrary = Cocoa_Vulkan_LoadLibrary; + device->Vulkan_UnloadLibrary = Cocoa_Vulkan_UnloadLibrary; + device->Vulkan_GetInstanceExtensions = Cocoa_Vulkan_GetInstanceExtensions; + device->Vulkan_CreateSurface = Cocoa_Vulkan_CreateSurface; + device->Vulkan_DestroySurface = Cocoa_Vulkan_DestroySurface; +#endif + +#ifdef SDL_VIDEO_METAL + device->Metal_CreateView = Cocoa_Metal_CreateView; + device->Metal_DestroyView = Cocoa_Metal_DestroyView; + device->Metal_GetLayer = Cocoa_Metal_GetLayer; +#endif + + device->StartTextInput = Cocoa_StartTextInput; + device->StopTextInput = Cocoa_StopTextInput; + device->UpdateTextInputArea = Cocoa_UpdateTextInputArea; + + device->SetClipboardData = Cocoa_SetClipboardData; + device->GetClipboardData = Cocoa_GetClipboardData; + device->HasClipboardData = Cocoa_HasClipboardData; + + device->free = Cocoa_DeleteDevice; + + device->device_caps = VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT | + VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS; + return device; + } +} + +VideoBootStrap COCOA_bootstrap = { + "cocoa", "SDL Cocoa video driver", + Cocoa_CreateDevice, + Cocoa_ShowMessageBox, + false +}; + +static bool Cocoa_VideoInit(SDL_VideoDevice *_this) +{ + @autoreleasepool { + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + + Cocoa_InitModes(_this); + Cocoa_InitKeyboard(_this); + if (!Cocoa_InitMouse(_this)) { + return false; + } + if (!Cocoa_InitPen(_this)) { + return false; + } + + // Assume we have a mouse and keyboard + // We could use GCMouse and GCKeyboard if we needed to, as is done in SDL_uikitevents.m + SDL_AddKeyboard(SDL_DEFAULT_KEYBOARD_ID, NULL, false); + SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL, false); + + data.allow_spaces = SDL_GetHintBoolean(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, true); + data.trackpad_is_touch_only = SDL_GetHintBoolean(SDL_HINT_TRACKPAD_IS_TOUCH_ONLY, false); + SDL_AddHintCallback(SDL_HINT_VIDEO_MAC_FULLSCREEN_MENU_VISIBILITY, Cocoa_MenuVisibilityCallback, NULL); + + data.swaplock = SDL_CreateMutex(); + if (!data.swaplock) { + return false; + } + + return true; + } +} + +void Cocoa_VideoQuit(SDL_VideoDevice *_this) +{ + @autoreleasepool { + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + Cocoa_QuitModes(_this); + Cocoa_QuitKeyboard(_this); + Cocoa_QuitMouse(_this); + Cocoa_QuitPen(_this); + SDL_DestroyMutex(data.swaplock); + data.swaplock = NULL; + } +} + +// This function assumes that it's called from within an autorelease pool +SDL_SystemTheme Cocoa_GetSystemTheme(void) +{ + if (@available(macOS 10.14, *)) { + NSAppearance* appearance = [[NSApplication sharedApplication] effectiveAppearance]; + + if ([appearance.name containsString: @"Dark"]) { + return SDL_SYSTEM_THEME_DARK; + } + } + return SDL_SYSTEM_THEME_LIGHT; +} + +// This function assumes that it's called from within an autorelease pool +NSImage *Cocoa_CreateImage(SDL_Surface *surface) +{ + NSImage *img; + + img = [[NSImage alloc] initWithSize:NSMakeSize(surface->w, surface->h)]; + if (img == nil) { + return nil; + } + + SDL_Surface **images = SDL_GetSurfaceImages(surface, NULL); + if (!images) { + return nil; + } + + for (int i = 0; images[i]; ++i) { + SDL_Surface *converted = SDL_ConvertSurface(images[i], SDL_PIXELFORMAT_RGBA32); + if (!converted) { + SDL_free(images); + return nil; + } + + // Premultiply the alpha channel + SDL_PremultiplySurfaceAlpha(converted, false); + + NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL + pixelsWide:converted->w + pixelsHigh:converted->h + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:converted->pitch + bitsPerPixel:SDL_BITSPERPIXEL(converted->format)]; + if (imgrep == nil) { + SDL_free(images); + SDL_DestroySurface(converted); + return nil; + } + + // Copy the pixels + Uint8 *pixels = [imgrep bitmapData]; + SDL_memcpy(pixels, converted->pixels, (size_t)converted->h * converted->pitch); + SDL_DestroySurface(converted); + + // Add the image representation + [img addRepresentation:imgrep]; + } + SDL_free(images); + + return img; +} + +/* + * macOS log support. + * + * This doesn't really have anything to do with the interfaces of the SDL video + * subsystem, but we need to stuff this into an Objective-C source code file. + * + * NOTE: This is copypasted in src/video/uikit/SDL_uikitvideo.m! Be sure both + * versions remain identical! + */ + +void SDL_NSLog(const char *prefix, const char *text) +{ + @autoreleasepool { + NSString *nsText = [NSString stringWithUTF8String:text]; + if (prefix && *prefix) { + NSString *nsPrefix = [NSString stringWithUTF8String:prefix]; + NSLog(@"%@%@", nsPrefix, nsText); + } else { + NSLog(@"%@", nsText); + } + } +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/* + * @author Mark Callow, www.edgewise-consulting.com. Based on Jacob Lifshay's + * SDL_x11vulkan.h. + */ + +#include "SDL_internal.h" + +#ifndef SDL_cocoavulkan_h_ +#define SDL_cocoavulkan_h_ + +#include "../SDL_vulkan_internal.h" +#include "../SDL_sysvideo.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_COCOA) + +extern bool Cocoa_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path); +extern void Cocoa_Vulkan_UnloadLibrary(SDL_VideoDevice *_this); +extern char const* const* Cocoa_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, Uint32 *count); +extern bool Cocoa_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface); +extern void Cocoa_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator); + +#endif + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/* + * @author Mark Callow, www.edgewise-consulting.com. Based on Jacob Lifshay's + * SDL_x11vulkan.c. + */ +#include "SDL_internal.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_COCOA) + +#include "SDL_cocoavideo.h" +#include "SDL_cocoawindow.h" + +#include "SDL_cocoametalview.h" +#include "SDL_cocoavulkan.h" + +#include + +const char *defaultPaths[] = { + "vulkan.framework/vulkan", + "libvulkan.1.dylib", + "libvulkan.dylib", + "MoltenVK.framework/MoltenVK", + "libMoltenVK.dylib" +}; + +// Since libSDL is most likely a .dylib, need RTLD_DEFAULT not RTLD_SELF. +#define DEFAULT_HANDLE RTLD_DEFAULT + +bool Cocoa_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + VkExtensionProperties *extensions = NULL; + Uint32 extensionCount = 0; + bool hasSurfaceExtension = false; + bool hasMetalSurfaceExtension = false; + bool hasMacOSSurfaceExtension = false; + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL; + + if (_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan Portability library is already loaded."); + } + + // Load the Vulkan loader library + if (!path) { + path = SDL_GetHint(SDL_HINT_VULKAN_LIBRARY); + } + + if (!path) { + // Handle the case where Vulkan Portability is linked statically. + vkGetInstanceProcAddr = + (PFN_vkGetInstanceProcAddr)dlsym(DEFAULT_HANDLE, + "vkGetInstanceProcAddr"); + } + + if (vkGetInstanceProcAddr) { + _this->vulkan_config.loader_handle = DEFAULT_HANDLE; + } else { + const char **paths; + const char *foundPath = NULL; + int numPaths; + int i; + + if (path) { + paths = &path; + numPaths = 1; + } else { + /* Look for framework or .dylib packaged with the application + * instead. */ + paths = defaultPaths; + numPaths = SDL_arraysize(defaultPaths); + } + + for (i = 0; i < numPaths && _this->vulkan_config.loader_handle == NULL; i++) { + foundPath = paths[i]; + _this->vulkan_config.loader_handle = SDL_LoadObject(foundPath); + } + + if (_this->vulkan_config.loader_handle == NULL) { + return SDL_SetError("Failed to load Vulkan Portability library"); + } + + SDL_strlcpy(_this->vulkan_config.loader_path, foundPath, + SDL_arraysize(_this->vulkan_config.loader_path)); + vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)SDL_LoadFunction( + _this->vulkan_config.loader_handle, "vkGetInstanceProcAddr"); + } + + if (!vkGetInstanceProcAddr) { + SDL_SetError("Failed to find %s in either executable or %s: %s", + "vkGetInstanceProcAddr", + _this->vulkan_config.loader_path, + (const char *)dlerror()); + goto fail; + } + + _this->vulkan_config.vkGetInstanceProcAddr = (void *)vkGetInstanceProcAddr; + _this->vulkan_config.vkEnumerateInstanceExtensionProperties = + (void *)((PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr)( + VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties"); + if (!_this->vulkan_config.vkEnumerateInstanceExtensionProperties) { + goto fail; + } + extensions = SDL_Vulkan_CreateInstanceExtensionsList( + (PFN_vkEnumerateInstanceExtensionProperties) + _this->vulkan_config.vkEnumerateInstanceExtensionProperties, + &extensionCount); + if (!extensions) { + goto fail; + } + for (Uint32 i = 0; i < extensionCount; i++) { + if (SDL_strcmp(VK_KHR_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasSurfaceExtension = true; + } else if (SDL_strcmp(VK_EXT_METAL_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasMetalSurfaceExtension = true; + } else if (SDL_strcmp(VK_MVK_MACOS_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasMacOSSurfaceExtension = true; + } + } + SDL_free(extensions); + if (!hasSurfaceExtension) { + SDL_SetError("Installed Vulkan Portability library doesn't implement the " VK_KHR_SURFACE_EXTENSION_NAME " extension"); + goto fail; + } else if (!hasMetalSurfaceExtension && !hasMacOSSurfaceExtension) { + SDL_SetError("Installed Vulkan Portability library doesn't implement the " VK_EXT_METAL_SURFACE_EXTENSION_NAME " or " VK_MVK_MACOS_SURFACE_EXTENSION_NAME " extensions"); + goto fail; + } + return true; + +fail: + SDL_UnloadObject(_this->vulkan_config.loader_handle); + _this->vulkan_config.loader_handle = NULL; + return false; +} + +void Cocoa_Vulkan_UnloadLibrary(SDL_VideoDevice *_this) +{ + if (_this->vulkan_config.loader_handle) { + if (_this->vulkan_config.loader_handle != DEFAULT_HANDLE) { + SDL_UnloadObject(_this->vulkan_config.loader_handle); + } + _this->vulkan_config.loader_handle = NULL; + } +} + +char const* const* Cocoa_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, + Uint32 *count) +{ + static const char *const extensionsForCocoa[] = { + VK_KHR_SURFACE_EXTENSION_NAME, VK_EXT_METAL_SURFACE_EXTENSION_NAME, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME + }; + if(count) { + *count = SDL_arraysize(extensionsForCocoa); + } + return extensionsForCocoa; +} + +static bool Cocoa_Vulkan_CreateSurfaceViaMetalView(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface, + PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT, + PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK) +{ + VkResult rc; + SDL_MetalView metalview = Cocoa_Metal_CreateView(_this, window); + if (metalview == NULL) { + return false; + } + + if (vkCreateMetalSurfaceEXT) { + VkMetalSurfaceCreateInfoEXT createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.pLayer = (__bridge const CAMetalLayer *) + Cocoa_Metal_GetLayer(_this, metalview); + rc = vkCreateMetalSurfaceEXT(instance, &createInfo, allocator, surface); + if (rc != VK_SUCCESS) { + Cocoa_Metal_DestroyView(_this, metalview); + return SDL_SetError("vkCreateMetalSurfaceEXT failed: %s", SDL_Vulkan_GetResultString(rc)); + } + } else { + VkMacOSSurfaceCreateInfoMVK createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.pView = (const void *)metalview; + rc = vkCreateMacOSSurfaceMVK(instance, &createInfo, + NULL, surface); + if (rc != VK_SUCCESS) { + Cocoa_Metal_DestroyView(_this, metalview); + return SDL_SetError("vkCreateMacOSSurfaceMVK failed: %s", SDL_Vulkan_GetResultString(rc)); + } + } + + /* Unfortunately there's no SDL_Vulkan_DestroySurface function we can call + * Metal_DestroyView from. Right now the metal view's ref count is +2 (one + * from returning a new view object in CreateView, and one because it's + * a subview of the window.) If we release the view here to make it +1, it + * will be destroyed when the window is destroyed. + * + * TODO: Now that we have SDL_Vulkan_DestroySurface someone with enough + * knowledge of Metal can proceed. */ + CFBridgingRelease(metalview); + + return true; // success! +} + +bool Cocoa_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface) +{ + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = + (PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr; + PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT = + (PFN_vkCreateMetalSurfaceEXT)vkGetInstanceProcAddr( + instance, + "vkCreateMetalSurfaceEXT"); + PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK = + (PFN_vkCreateMacOSSurfaceMVK)vkGetInstanceProcAddr( + instance, + "vkCreateMacOSSurfaceMVK"); + VkResult rc; + + if (!_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan is not loaded"); + } + + if (!vkCreateMetalSurfaceEXT && !vkCreateMacOSSurfaceMVK) { + return SDL_SetError(VK_EXT_METAL_SURFACE_EXTENSION_NAME " or " VK_MVK_MACOS_SURFACE_EXTENSION_NAME + " extensions are not enabled in the Vulkan instance."); + } + + if (window->flags & SDL_WINDOW_EXTERNAL) { + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + if (![data.sdlContentView.layer isKindOfClass:[CAMetalLayer class]]) { + [data.sdlContentView setLayer:[CAMetalLayer layer]]; + } + + if (vkCreateMetalSurfaceEXT) { + VkMetalSurfaceCreateInfoEXT createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.pLayer = (CAMetalLayer *)data.sdlContentView.layer; + rc = vkCreateMetalSurfaceEXT(instance, &createInfo, allocator, surface); + if (rc != VK_SUCCESS) { + return SDL_SetError("vkCreateMetalSurfaceEXT failed: %s", SDL_Vulkan_GetResultString(rc)); + } + } else { + VkMacOSSurfaceCreateInfoMVK createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.pView = (__bridge const void *)data.sdlContentView; + rc = vkCreateMacOSSurfaceMVK(instance, &createInfo, + allocator, surface); + if (rc != VK_SUCCESS) { + return SDL_SetError("vkCreateMacOSSurfaceMVK failed: %s", SDL_Vulkan_GetResultString(rc)); + } + } + } + } else { + return Cocoa_Vulkan_CreateSurfaceViaMetalView(_this, window, instance, allocator, surface, vkCreateMetalSurfaceEXT, vkCreateMacOSSurfaceMVK); + } + + return true; +} + +void Cocoa_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator) +{ + if (_this->vulkan_config.loader_handle) { + SDL_Vulkan_DestroySurface_Internal(_this->vulkan_config.vkGetInstanceProcAddr, instance, surface, allocator); + // TODO: Add CFBridgingRelease(metalview) here perhaps? + } +} + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifndef SDL_cocoawindow_h_ +#define SDL_cocoawindow_h_ + +#import + +#ifdef SDL_VIDEO_OPENGL_EGL +#include "../SDL_egl_c.h" +#endif + +#define SDL_METALVIEW_TAG 255 + +@class SDL_CocoaWindowData; + +typedef enum +{ + PENDING_OPERATION_NONE = 0x00, + PENDING_OPERATION_ENTER_FULLSCREEN = 0x01, + PENDING_OPERATION_LEAVE_FULLSCREEN = 0x02, + PENDING_OPERATION_MINIMIZE = 0x04, + PENDING_OPERATION_ZOOM = 0x08 +} PendingWindowOperation; + +@interface SDL3Cocoa_WindowListener : NSResponder +{ + /* SDL_CocoaWindowData owns this Listener and has a strong reference to it. + * To avoid reference cycles, we could have either a weak or an + * unretained ref to the WindowData. */ + __weak SDL_CocoaWindowData *_data; + BOOL observingVisible; + BOOL wasCtrlLeft; + BOOL wasVisible; + BOOL isFullscreenSpace; + BOOL inFullscreenTransition; + PendingWindowOperation pendingWindowOperation; + BOOL isMoving; + BOOL isMiniaturizing; + NSInteger focusClickPending; + float pendingWindowWarpX, pendingWindowWarpY; + BOOL isDragAreaRunning; + NSTimer *liveResizeTimer; +} + +- (BOOL)isTouchFromTrackpad:(NSEvent *)theEvent; +- (void)listen:(SDL_CocoaWindowData *)data; +- (void)pauseVisibleObservation; +- (void)resumeVisibleObservation; +- (BOOL)setFullscreenSpace:(BOOL)state; +- (BOOL)isInFullscreenSpace; +- (BOOL)isInFullscreenSpaceTransition; +- (void)addPendingWindowOperation:(PendingWindowOperation)operation; +- (void)close; + +- (BOOL)isMoving; +- (BOOL)isMovingOrFocusClickPending; +- (void)setFocusClickPending:(NSInteger)button; +- (void)clearFocusClickPending:(NSInteger)button; +- (void)updateIgnoreMouseState:(NSEvent *)theEvent; +- (void)setPendingMoveX:(float)x Y:(float)y; +- (void)windowDidFinishMoving; +- (void)onMovingOrFocusClickPendingStateCleared; + +// Window delegate functionality +- (BOOL)windowShouldClose:(id)sender; +- (void)windowDidExpose:(NSNotification *)aNotification; +- (void)windowDidChangeOcclusionState:(NSNotification *)aNotification; +- (void)windowWillStartLiveResize:(NSNotification *)aNotification; +- (void)windowDidEndLiveResize:(NSNotification *)aNotification; +- (void)windowDidMove:(NSNotification *)aNotification; +- (void)windowDidResize:(NSNotification *)aNotification; +- (void)windowDidMiniaturize:(NSNotification *)aNotification; +- (void)windowDidDeminiaturize:(NSNotification *)aNotification; +- (void)windowDidBecomeKey:(NSNotification *)aNotification; +- (void)windowDidResignKey:(NSNotification *)aNotification; +- (void)windowDidChangeBackingProperties:(NSNotification *)aNotification; +- (void)windowDidChangeScreenProfile:(NSNotification *)aNotification; +- (void)windowDidChangeScreen:(NSNotification *)aNotification; +- (void)windowWillEnterFullScreen:(NSNotification *)aNotification; +- (void)windowDidEnterFullScreen:(NSNotification *)aNotification; +- (void)windowWillExitFullScreen:(NSNotification *)aNotification; +- (void)windowDidExitFullScreen:(NSNotification *)aNotification; +- (NSApplicationPresentationOptions)window:(NSWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions; + +// See if event is in a drag area, toggle on window dragging. +- (void)updateHitTest; +- (BOOL)processHitTest:(NSEvent *)theEvent; + +// Window event handling +- (void)mouseDown:(NSEvent *)theEvent; +- (void)rightMouseDown:(NSEvent *)theEvent; +- (void)otherMouseDown:(NSEvent *)theEvent; +- (void)mouseUp:(NSEvent *)theEvent; +- (void)rightMouseUp:(NSEvent *)theEvent; +- (void)otherMouseUp:(NSEvent *)theEvent; +- (void)mouseMoved:(NSEvent *)theEvent; +- (void)mouseDragged:(NSEvent *)theEvent; +- (void)rightMouseDragged:(NSEvent *)theEvent; +- (void)otherMouseDragged:(NSEvent *)theEvent; +- (void)scrollWheel:(NSEvent *)theEvent; +- (void)touchesBeganWithEvent:(NSEvent *)theEvent; +- (void)touchesMovedWithEvent:(NSEvent *)theEvent; +- (void)touchesEndedWithEvent:(NSEvent *)theEvent; +- (void)touchesCancelledWithEvent:(NSEvent *)theEvent; + +// Touch event handling +- (void)handleTouches:(NSTouchPhase)phase withEvent:(NSEvent *)theEvent; + +// Tablet event handling (but these also come through on mouse events sometimes!) +- (void)tabletProximity:(NSEvent *)theEvent; +- (void)tabletPoint:(NSEvent *)theEvent; + +@end +/* *INDENT-ON* */ + +@class SDL3OpenGLContext; +@class SDL_CocoaVideoData; + +@interface SDL_CocoaWindowData : NSObject +@property(nonatomic) SDL_Window *window; +@property(nonatomic) NSWindow *nswindow; +@property(nonatomic) NSView *sdlContentView; +@property(nonatomic) NSMutableArray *nscontexts; +@property(nonatomic) BOOL in_blocking_transition; +@property(nonatomic) BOOL fullscreen_space_requested; +@property(nonatomic) BOOL was_zoomed; +@property(nonatomic) NSInteger window_number; +@property(nonatomic) NSInteger flash_request; +@property(nonatomic) SDL_Window *keyboard_focus; +@property(nonatomic) SDL3Cocoa_WindowListener *listener; +@property(nonatomic) NSModalSession modal_session; +@property(nonatomic) SDL_CocoaVideoData *videodata; +@property(nonatomic) bool pending_size; +@property(nonatomic) bool pending_position; +@property(nonatomic) bool border_toggled; + +#ifdef SDL_VIDEO_OPENGL_EGL +@property(nonatomic) EGLSurface egl_surface; +#endif +@end + +extern bool b_inModeTransition; + +extern bool Cocoa_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); +extern void Cocoa_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon); +extern bool Cocoa_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h); +extern bool Cocoa_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity); +extern void Cocoa_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void Cocoa_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered); +extern void Cocoa_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable); +extern void Cocoa_SetWindowAlwaysOnTop(SDL_VideoDevice *_this, SDL_Window *window, bool on_top); +extern SDL_FullscreenResult Cocoa_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen); +extern void *Cocoa_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size); +extern SDL_DisplayID Cocoa_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_SetWindowMouseRect(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_SetWindowMouseGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed); +extern void Cocoa_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern bool Cocoa_SetWindowHitTest(SDL_Window *window, bool enabled); +extern void Cocoa_AcceptDragAndDrop(SDL_Window *window, bool accept); +extern bool Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation); +extern bool Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable); +extern bool Cocoa_SetWindowModal(SDL_VideoDevice *_this, SDL_Window *window, bool modal); +extern bool Cocoa_SetWindowParent(SDL_VideoDevice *_this, SDL_Window *window, SDL_Window *parent); +extern bool Cocoa_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window); + +extern void Cocoa_MenuVisibilityCallback(void *userdata, const char *name, const char *oldValue, const char *newValue); + +#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 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include // For FLT_MAX + +#include "../../events/SDL_dropevents_c.h" +#include "../../events/SDL_keyboard_c.h" +#include "../../events/SDL_mouse_c.h" +#include "../../events/SDL_touch_c.h" +#include "../../events/SDL_windowevents_c.h" +#include "../SDL_sysvideo.h" + +#include "SDL_cocoamouse.h" +#include "SDL_cocoaopengl.h" +#include "SDL_cocoaopengles.h" +#include "SDL_cocoavideo.h" + +#if 0 +#define DEBUG_COCOAWINDOW +#endif + +#ifdef DEBUG_COCOAWINDOW +#define DLog(fmt, ...) printf("%s: " fmt "\n", __func__, ##__VA_ARGS__) +#else +#define DLog(...) \ + do { \ + } while (0) +#endif + +#ifndef MAC_OS_X_VERSION_10_12 +#define NSEventModifierFlagCapsLock NSAlphaShiftKeyMask +#endif +#ifndef NSAppKitVersionNumber10_13_2 +#define NSAppKitVersionNumber10_13_2 1561.2 +#endif +#ifndef NSAppKitVersionNumber10_14 +#define NSAppKitVersionNumber10_14 1671 +#endif + +@implementation SDL_CocoaWindowData + +@end + +@interface NSScreen (SDL) +#if MAC_OS_X_VERSION_MAX_ALLOWED < 120000 // Added in the 12.0 SDK +@property(readonly) NSEdgeInsets safeAreaInsets; +#endif +@end + +@interface NSWindow (SDL) +// This is available as of 10.13.2, but isn't in public headers +@property(nonatomic) NSRect mouseConfinementRect; +@end + +@interface SDL3Window : NSWindow +// These are needed for borderless/fullscreen windows +- (BOOL)canBecomeKeyWindow; +- (BOOL)canBecomeMainWindow; +- (void)sendEvent:(NSEvent *)event; +- (void)doCommandBySelector:(SEL)aSelector; + +// Handle drag-and-drop of files onto the SDL window. +- (NSDragOperation)draggingEntered:(id)sender; +- (void)draggingExited:(id)sender; +- (NSDragOperation)draggingUpdated:(id)sender; +- (BOOL)performDragOperation:(id)sender; +- (BOOL)wantsPeriodicDraggingUpdates; +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem; + +- (SDL_Window *)findSDLWindow; +@end + +@implementation SDL3Window + +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem +{ + /* Only allow using the macOS native fullscreen toggle menubar item if the + * window is resizable and not in a SDL fullscreen mode. + */ + if ([menuItem action] == @selector(toggleFullScreen:)) { + SDL_Window *window = [self findSDLWindow]; + if (!window) { + return NO; + } + + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + if ((window->flags & SDL_WINDOW_FULLSCREEN) && ![data.listener isInFullscreenSpace]) { + return NO; + } else if (!(window->flags & SDL_WINDOW_RESIZABLE)) { + return NO; + } + } + return [super validateMenuItem:menuItem]; +} + +- (BOOL)canBecomeKeyWindow +{ + SDL_Window *window = [self findSDLWindow]; + if (window && !(window->flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_NOT_FOCUSABLE))) { + return YES; + } else { + return NO; + } +} + +- (BOOL)canBecomeMainWindow +{ + SDL_Window *window = [self findSDLWindow]; + if (window && !(window->flags & (SDL_WINDOW_TOOLTIP | SDL_WINDOW_NOT_FOCUSABLE)) && !SDL_WINDOW_IS_POPUP(window)) { + return YES; + } else { + return NO; + } +} + +- (void)sendEvent:(NSEvent *)event +{ + id delegate; + [super sendEvent:event]; + + if ([event type] != NSEventTypeLeftMouseUp) { + return; + } + + delegate = [self delegate]; + if (![delegate isKindOfClass:[SDL3Cocoa_WindowListener class]]) { + return; + } + + if ([delegate isMoving]) { + [delegate windowDidFinishMoving]; + } +} + +/* We'll respond to selectors by doing nothing so we don't beep. + * The escape key gets converted to a "cancel" selector, etc. + */ +- (void)doCommandBySelector:(SEL)aSelector +{ + // NSLog(@"doCommandBySelector: %@\n", NSStringFromSelector(aSelector)); +} + +- (NSDragOperation)draggingEntered:(id)sender +{ + if (([sender draggingSourceOperationMask] & NSDragOperationGeneric) == NSDragOperationGeneric) { + return NSDragOperationGeneric; + } else if (([sender draggingSourceOperationMask] & NSDragOperationCopy) == NSDragOperationCopy) { + return NSDragOperationCopy; + } + + return NSDragOperationNone; // no idea what to do with this, reject it. +} + +- (void)draggingExited:(id)sender +{ + SDL_Window *sdlwindow = [self findSDLWindow]; + SDL_SendDropComplete(sdlwindow); +} + +- (NSDragOperation)draggingUpdated:(id)sender +{ + if (([sender draggingSourceOperationMask] & NSDragOperationGeneric) == NSDragOperationGeneric) { + SDL_Window *sdlwindow = [self findSDLWindow]; + NSPoint point = [sender draggingLocation]; + float x, y; + x = point.x; + y = (sdlwindow->h - point.y); + SDL_SendDropPosition(sdlwindow, x, y); + return NSDragOperationGeneric; + } else if (([sender draggingSourceOperationMask] & NSDragOperationCopy) == NSDragOperationCopy) { + SDL_Window *sdlwindow = [self findSDLWindow]; + NSPoint point = [sender draggingLocation]; + float x, y; + x = point.x; + y = (sdlwindow->h - point.y); + SDL_SendDropPosition(sdlwindow, x, y); + return NSDragOperationCopy; + } + + return NSDragOperationNone; // no idea what to do with this, reject it. +} + +- (BOOL)performDragOperation:(id)sender +{ + SDL_LogTrace(SDL_LOG_CATEGORY_INPUT, + ". [SDL] In performDragOperation, draggingSourceOperationMask %lx, " + "expected Generic %lx, others Copy %lx, Link %lx, Private %lx, Move %lx, Delete %lx\n", + (unsigned long)[sender draggingSourceOperationMask], + (unsigned long)NSDragOperationGeneric, + (unsigned long)NSDragOperationCopy, + (unsigned long)NSDragOperationLink, + (unsigned long)NSDragOperationPrivate, + (unsigned long)NSDragOperationMove, + (unsigned long)NSDragOperationDelete); + if ([sender draggingPasteboard]) { + SDL_LogTrace(SDL_LOG_CATEGORY_INPUT, + ". [SDL] In performDragOperation, valid draggingPasteboard, " + "name [%s] '%s', changeCount %ld\n", + [[[[sender draggingPasteboard] name] className] UTF8String], + [[[[sender draggingPasteboard] name] description] UTF8String], + (long)[[sender draggingPasteboard] changeCount]); + } + @autoreleasepool { + NSPasteboard *pasteboard = [sender draggingPasteboard]; + NSString *desiredType = [pasteboard availableTypeFromArray:@[ NSFilenamesPboardType, NSPasteboardTypeString ]]; + SDL_Window *sdlwindow = [self findSDLWindow]; + NSData *pboardData; + id pboardPlist; + NSString *pboardString; + NSPoint point; + float x, y; + + for (NSString *supportedType in [pasteboard types]) { + NSString *typeString = [pasteboard stringForType:supportedType]; + SDL_LogTrace(SDL_LOG_CATEGORY_INPUT, + ". [SDL] In performDragOperation, Pasteboard type '%s', stringForType (%lu) '%s'\n", + [[supportedType description] UTF8String], + (unsigned long)[[typeString description] length], + [[typeString description] UTF8String]); + } + + if (desiredType == nil) { + return NO; // can't accept anything that's being dropped here. + } + pboardData = [pasteboard dataForType:desiredType]; + if (pboardData == nil) { + return NO; + } + SDL_assert([desiredType isEqualToString:NSFilenamesPboardType] || + [desiredType isEqualToString:NSPasteboardTypeString]); + + pboardString = [pasteboard stringForType:desiredType]; + pboardPlist = [pasteboard propertyListForType:desiredType]; + + // Use SendDropPosition to update the mouse location + point = [sender draggingLocation]; + x = point.x; + y = (sdlwindow->h - point.y); + if (x >= 0.0f && x < (float)sdlwindow->w && y >= 0.0f && y < (float)sdlwindow->h) { + SDL_SendDropPosition(sdlwindow, x, y); + } + // Use SendDropPosition to update the mouse location + + if ([desiredType isEqualToString:NSFilenamesPboardType]) { + for (NSString *path in (NSArray *)pboardPlist) { + NSURL *fileURL = [NSURL fileURLWithPath:path]; + NSNumber *isAlias = nil; + + [fileURL getResourceValue:&isAlias forKey:NSURLIsAliasFileKey error:nil]; + + // If the URL is an alias, resolve it. + if ([isAlias boolValue]) { + NSURLBookmarkResolutionOptions opts = NSURLBookmarkResolutionWithoutMounting | + NSURLBookmarkResolutionWithoutUI; + NSData *bookmark = [NSURL bookmarkDataWithContentsOfURL:fileURL error:nil]; + if (bookmark != nil) { + NSURL *resolvedURL = [NSURL URLByResolvingBookmarkData:bookmark + options:opts + relativeToURL:nil + bookmarkDataIsStale:nil + error:nil]; + if (resolvedURL != nil) { + fileURL = resolvedURL; + } + } + } + SDL_LogTrace(SDL_LOG_CATEGORY_INPUT, + ". [SDL] In performDragOperation, desiredType '%s', " + "Submitting DropFile as (%lu) '%s'\n", + [[desiredType description] UTF8String], + (unsigned long)[[[fileURL path] description] length], + [[[fileURL path] description] UTF8String]); + if (!SDL_SendDropFile(sdlwindow, NULL, [[[fileURL path] description] UTF8String])) { + return NO; + } + } + } else if ([desiredType isEqualToString:NSPasteboardTypeString]) { + char *buffer = SDL_strdup([[pboardString description] UTF8String]); + char *saveptr = NULL; + char *token = SDL_strtok_r(buffer, "\r\n", &saveptr); + while (token) { + SDL_LogTrace(SDL_LOG_CATEGORY_INPUT, + ". [SDL] In performDragOperation, desiredType '%s', " + "Submitting DropText as (%lu) '%s'\n", + [[desiredType description] UTF8String], + SDL_strlen(token), token); + if (!SDL_SendDropText(sdlwindow, token)) { + SDL_free(buffer); + return NO; + } + token = SDL_strtok_r(NULL, "\r\n", &saveptr); + } + SDL_free(buffer); + } + + SDL_SendDropComplete(sdlwindow); + return YES; + } +} + +- (BOOL)wantsPeriodicDraggingUpdates +{ + return NO; +} + +- (SDL_Window *)findSDLWindow +{ + SDL_Window *sdlwindow = NULL; + SDL_VideoDevice *_this = SDL_GetVideoDevice(); + + // !!! FIXME: is there a better way to do this? + if (_this) { + for (sdlwindow = _this->windows; sdlwindow; sdlwindow = sdlwindow->next) { + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)sdlwindow->internal).nswindow; + if (nswindow == self) { + break; + } + } + } + + return sdlwindow; +} + +@end + +bool b_inModeTransition; + +static CGFloat SqDistanceToRect(const NSPoint *point, const NSRect *rect) +{ + NSPoint edge = *point; + CGFloat left = NSMinX(*rect), right = NSMaxX(*rect); + CGFloat bottom = NSMinX(*rect), top = NSMaxY(*rect); + NSPoint delta; + + if (point->x < left) { + edge.x = left; + } else if (point->x > right) { + edge.x = right; + } + + if (point->y < bottom) { + edge.y = bottom; + } else if (point->y > top) { + edge.y = top; + } + + delta = NSMakePoint(edge.x - point->x, edge.y - point->y); + return delta.x * delta.x + delta.y * delta.y; +} + +static NSScreen *ScreenForPoint(const NSPoint *point) +{ + NSScreen *screen; + + // Do a quick check first to see if the point lies on a specific screen + for (NSScreen *candidate in [NSScreen screens]) { + if (NSPointInRect(*point, [candidate frame])) { + screen = candidate; + break; + } + } + + // Find the screen the point is closest to + if (!screen) { + CGFloat closest = MAXFLOAT; + for (NSScreen *candidate in [NSScreen screens]) { + NSRect screenRect = [candidate frame]; + + CGFloat sqdist = SqDistanceToRect(point, &screenRect); + if (sqdist < closest) { + screen = candidate; + closest = sqdist; + } + } + } + + return screen; +} + +bool Cocoa_IsWindowInFullscreenSpace(SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + if ([data.listener isInFullscreenSpace]) { + return true; + } else { + return false; + } + } +} + +bool Cocoa_IsWindowZoomed(SDL_Window *window) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = data.nswindow; + bool zoomed = false; + + // isZoomed always returns true if the window is not resizable or the window is fullscreen + if ((window->flags & SDL_WINDOW_RESIZABLE) && [nswindow isZoomed] && + !(window->flags & SDL_WINDOW_FULLSCREEN) && !Cocoa_IsWindowInFullscreenSpace(window)) { + // If we are at our desired floating area, then we're not zoomed + bool floating = (window->x == window->floating.x && + window->y == window->floating.y && + window->w == window->floating.w && + window->h == window->floating.h); + if (!floating) { + zoomed = true; + } + } + return zoomed; +} + +typedef enum CocoaMenuVisibility +{ + COCOA_MENU_VISIBILITY_AUTO = 0, + COCOA_MENU_VISIBILITY_NEVER, + COCOA_MENU_VISIBILITY_ALWAYS +} CocoaMenuVisibility; + +static CocoaMenuVisibility menu_visibility_hint = COCOA_MENU_VISIBILITY_AUTO; + +static void Cocoa_ToggleFullscreenSpaceMenuVisibility(SDL_Window *window) +{ + if (window && Cocoa_IsWindowInFullscreenSpace(window)) { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + // 'Auto' sets the menu to visible if fullscreen wasn't explicitly entered via SDL_SetWindowFullscreen(). + if ((menu_visibility_hint == COCOA_MENU_VISIBILITY_AUTO && !data.fullscreen_space_requested) || + menu_visibility_hint == COCOA_MENU_VISIBILITY_ALWAYS) { + [NSMenu setMenuBarVisible:YES]; + } else { + [NSMenu setMenuBarVisible:NO]; + } + } +} + +void Cocoa_MenuVisibilityCallback(void *userdata, const char *name, const char *oldValue, const char *newValue) +{ + if (newValue) { + if (*newValue == '0' || SDL_strcasecmp(newValue, "false") == 0) { + menu_visibility_hint = COCOA_MENU_VISIBILITY_NEVER; + } else if (*newValue == '1' || SDL_strcasecmp(newValue, "true") == 0) { + menu_visibility_hint = COCOA_MENU_VISIBILITY_ALWAYS; + } else { + menu_visibility_hint = COCOA_MENU_VISIBILITY_AUTO; + } + } else { + menu_visibility_hint = COCOA_MENU_VISIBILITY_AUTO; + } + + // Update the current menu visibility. + Cocoa_ToggleFullscreenSpaceMenuVisibility(SDL_GetKeyboardFocus()); +} + +static NSScreen *ScreenForRect(const NSRect *rect) +{ + NSPoint center = NSMakePoint(NSMidX(*rect), NSMidY(*rect)); + return ScreenForPoint(¢er); +} + +static void ConvertNSRect(NSRect *r) +{ + r->origin.y = CGDisplayPixelsHigh(kCGDirectMainDisplay) - r->origin.y - r->size.height; +} + +static void ScheduleContextUpdates(SDL_CocoaWindowData *data) +{ +// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it. +#ifdef SDL_VIDEO_OPENGL + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + + NSOpenGLContext *currentContext; + NSMutableArray *contexts; + if (!data || !data.nscontexts) { + return; + } + + currentContext = [NSOpenGLContext currentContext]; + contexts = data.nscontexts; + @synchronized(contexts) { + for (SDL3OpenGLContext *context in contexts) { + if (context == currentContext) { + [context update]; + } else { + [context scheduleUpdate]; + } + } + } + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // SDL_VIDEO_OPENGL +} + +// !!! FIXME: this should use a hint callback. +static bool GetHintCtrlClickEmulateRightClick(void) +{ + return SDL_GetHintBoolean(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, false); +} + +static NSUInteger GetWindowWindowedStyle(SDL_Window *window) +{ + /* IF YOU CHANGE ANY FLAGS IN HERE, PLEASE READ + the NSWindowStyleMaskBorderless comments in SetupWindowData()! */ + + /* always allow miniaturization, otherwise you can't programmatically + minimize the window, whether there's a title bar or not */ + NSUInteger style = NSWindowStyleMaskMiniaturizable; + + if (!SDL_WINDOW_IS_POPUP(window)) { + if (window->flags & SDL_WINDOW_BORDERLESS) { + style |= NSWindowStyleMaskBorderless; + } else { + style |= (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable); + } + if (window->flags & SDL_WINDOW_RESIZABLE) { + style |= NSWindowStyleMaskResizable; + } + } else { + style |= NSWindowStyleMaskBorderless; + } + return style; +} + +static NSUInteger GetWindowStyle(SDL_Window *window) +{ + NSUInteger style = 0; + + if (window->flags & SDL_WINDOW_FULLSCREEN) { + style = NSWindowStyleMaskBorderless; + } else { + style = GetWindowWindowedStyle(window); + } + return style; +} + +static bool SetWindowStyle(SDL_Window *window, NSUInteger style) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = data.nswindow; + + // The view responder chain gets messed with during setStyleMask + if ([data.sdlContentView nextResponder] == data.listener) { + [data.sdlContentView setNextResponder:nil]; + } + + [nswindow setStyleMask:style]; + + // The view responder chain gets messed with during setStyleMask + if ([data.sdlContentView nextResponder] != data.listener) { + [data.sdlContentView setNextResponder:data.listener]; + } + + return true; +} + +static bool ShouldAdjustCoordinatesForGrab(SDL_Window *window) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + if (!data || [data.listener isMovingOrFocusClickPending]) { + return false; + } + + if (!(window->flags & SDL_WINDOW_INPUT_FOCUS)) { + return false; + } + + if ((window->flags & SDL_WINDOW_MOUSE_GRABBED) || (window->mouse_rect.w > 0 && window->mouse_rect.h > 0)) { + return true; + } + return false; +} + +static bool AdjustCoordinatesForGrab(SDL_Window *window, float x, float y, CGPoint *adjusted) +{ + if (window->mouse_rect.w > 0 && window->mouse_rect.h > 0) { + SDL_Rect window_rect; + SDL_Rect mouse_rect; + + window_rect.x = 0; + window_rect.y = 0; + window_rect.w = window->w; + window_rect.h = window->h; + + if (SDL_GetRectIntersection(&window->mouse_rect, &window_rect, &mouse_rect)) { + float left = (float)window->x + mouse_rect.x; + float right = left + mouse_rect.w - 1; + float top = (float)window->y + mouse_rect.y; + float bottom = top + mouse_rect.h - 1; + if (x < left || x > right || y < top || y > bottom) { + adjusted->x = SDL_clamp(x, left, right); + adjusted->y = SDL_clamp(y, top, bottom); + return true; + } + return false; + } + } + + if (window->flags & SDL_WINDOW_MOUSE_GRABBED) { + float left = (float)window->x; + float right = left + window->w - 1; + float top = (float)window->y; + float bottom = top + window->h - 1; + if (x < left || x > right || y < top || y > bottom) { + adjusted->x = SDL_clamp(x, left, right); + adjusted->y = SDL_clamp(y, top, bottom); + return true; + } + } + return false; +} + +static void Cocoa_UpdateClipCursor(SDL_Window *window) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + if (NSAppKitVersionNumber >= NSAppKitVersionNumber10_13_2) { + NSWindow *nswindow = data.nswindow; + SDL_Rect mouse_rect; + + SDL_zero(mouse_rect); + + if (ShouldAdjustCoordinatesForGrab(window)) { + SDL_Rect window_rect; + + window_rect.x = 0; + window_rect.y = 0; + window_rect.w = window->w; + window_rect.h = window->h; + + if (window->mouse_rect.w > 0 && window->mouse_rect.h > 0) { + SDL_GetRectIntersection(&window->mouse_rect, &window_rect, &mouse_rect); + } + + if ((window->flags & SDL_WINDOW_MOUSE_GRABBED) != 0 && + SDL_RectEmpty(&mouse_rect)) { + SDL_memcpy(&mouse_rect, &window_rect, sizeof(mouse_rect)); + } + } + + if (SDL_RectEmpty(&mouse_rect)) { + nswindow.mouseConfinementRect = NSZeroRect; + } else { + NSRect rect; + rect.origin.x = mouse_rect.x; + rect.origin.y = [nswindow contentLayoutRect].size.height - mouse_rect.y - mouse_rect.h; + rect.size.width = mouse_rect.w; + rect.size.height = mouse_rect.h; + nswindow.mouseConfinementRect = rect; + } + } else { + // Move the cursor to the nearest point in the window + if (ShouldAdjustCoordinatesForGrab(window)) { + float x, y; + CGPoint cgpoint; + + SDL_GetGlobalMouseState(&x, &y); + if (AdjustCoordinatesForGrab(window, x, y, &cgpoint)) { + Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y); + CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint); + } + } + } +} + +static SDL_Window *GetParentToplevelWindow(SDL_Window *window) +{ + SDL_Window *toplevel = window; + + // Find the topmost parent + while (SDL_WINDOW_IS_POPUP(toplevel)) { + toplevel = toplevel->parent; + } + + return toplevel; +} + +static void Cocoa_SetKeyboardFocus(SDL_Window *window, bool set_active_focus) +{ + SDL_Window *toplevel = GetParentToplevelWindow(window); + SDL_CocoaWindowData *toplevel_data; + + toplevel_data = (__bridge SDL_CocoaWindowData *)toplevel->internal; + toplevel_data.keyboard_focus = window; + + if (set_active_focus && !window->is_hiding && !window->is_destroying) { + SDL_SetKeyboardFocus(window); + } +} + +static void Cocoa_SendExposedEventIfVisible(SDL_Window *window) +{ + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + if ([nswindow occlusionState] & NSWindowOcclusionStateVisible) { + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_EXPOSED, 0, 0); + } +} + +static void Cocoa_WaitForMiniaturizable(SDL_Window *window) +{ + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + NSButton *button = [nswindow standardWindowButton:NSWindowMiniaturizeButton]; + if (button) { + int iterations = 0; + while (![button isEnabled] && (iterations < 100)) { + SDL_Delay(10); + SDL_PumpEvents(); + iterations++; + } + } +} + +static NSCursor *Cocoa_GetDesiredCursor(void) +{ + SDL_Mouse *mouse = SDL_GetMouse(); + + if (mouse->cursor_shown && mouse->cur_cursor && !mouse->relative_mode) { + return (__bridge NSCursor *)mouse->cur_cursor->internal; + } + + return [NSCursor invisibleCursor]; +} + +@implementation SDL3Cocoa_WindowListener + +- (void)listen:(SDL_CocoaWindowData *)data +{ + NSNotificationCenter *center; + NSWindow *window = data.nswindow; + NSView *view = data.sdlContentView; + + _data = data; + observingVisible = YES; + wasCtrlLeft = NO; + wasVisible = [window isVisible]; + isFullscreenSpace = NO; + inFullscreenTransition = NO; + pendingWindowOperation = PENDING_OPERATION_NONE; + isMoving = NO; + isMiniaturizing = NO; + isDragAreaRunning = NO; + pendingWindowWarpX = pendingWindowWarpY = FLT_MAX; + liveResizeTimer = nil; + + center = [NSNotificationCenter defaultCenter]; + + if ([window delegate] != nil) { + [center addObserver:self selector:@selector(windowDidExpose:) name:NSWindowDidExposeNotification object:window]; + [center addObserver:self selector:@selector(windowDidChangeOcclusionState:) name:NSWindowDidChangeOcclusionStateNotification object:window]; + [center addObserver:self selector:@selector(windowWillStartLiveResize:) name:NSWindowWillStartLiveResizeNotification object:window]; + [center addObserver:self selector:@selector(windowDidEndLiveResize:) name:NSWindowDidEndLiveResizeNotification object:window]; + [center addObserver:self selector:@selector(windowWillMove:) name:NSWindowWillMoveNotification object:window]; + [center addObserver:self selector:@selector(windowDidMove:) name:NSWindowDidMoveNotification object:window]; + [center addObserver:self selector:@selector(windowDidResize:) name:NSWindowDidResizeNotification object:window]; + [center addObserver:self selector:@selector(windowWillMiniaturize:) name:NSWindowWillMiniaturizeNotification object:window]; + [center addObserver:self selector:@selector(windowDidMiniaturize:) name:NSWindowDidMiniaturizeNotification object:window]; + [center addObserver:self selector:@selector(windowDidDeminiaturize:) name:NSWindowDidDeminiaturizeNotification object:window]; + [center addObserver:self selector:@selector(windowDidBecomeKey:) name:NSWindowDidBecomeKeyNotification object:window]; + [center addObserver:self selector:@selector(windowDidResignKey:) name:NSWindowDidResignKeyNotification object:window]; + [center addObserver:self selector:@selector(windowDidChangeBackingProperties:) name:NSWindowDidChangeBackingPropertiesNotification object:window]; + [center addObserver:self selector:@selector(windowDidChangeScreenProfile:) name:NSWindowDidChangeScreenProfileNotification object:window]; + [center addObserver:self selector:@selector(windowDidChangeScreen:) name:NSWindowDidChangeScreenNotification object:window]; + [center addObserver:self selector:@selector(windowWillEnterFullScreen:) name:NSWindowWillEnterFullScreenNotification object:window]; + [center addObserver:self selector:@selector(windowDidEnterFullScreen:) name:NSWindowDidEnterFullScreenNotification object:window]; + [center addObserver:self selector:@selector(windowWillExitFullScreen:) name:NSWindowWillExitFullScreenNotification object:window]; + [center addObserver:self selector:@selector(windowDidExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:window]; + [center addObserver:self selector:@selector(windowDidFailToEnterFullScreen:) name:@"NSWindowDidFailToEnterFullScreenNotification" object:window]; + [center addObserver:self selector:@selector(windowDidFailToExitFullScreen:) name:@"NSWindowDidFailToExitFullScreenNotification" object:window]; + } else { + [window setDelegate:self]; + } + + /* Haven't found a delegate / notification that triggers when the window is + * ordered out (is not visible any more). You can be ordered out without + * minimizing, so DidMiniaturize doesn't work. (e.g. -[NSWindow orderOut:]) + */ + [window addObserver:self + forKeyPath:@"visible" + options:NSKeyValueObservingOptionNew + context:NULL]; + + [window setNextResponder:self]; + [window setAcceptsMouseMovedEvents:YES]; + + [view setNextResponder:self]; + + [view setAcceptsTouchEvents:YES]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if (!observingVisible) { + return; + } + + if (object == _data.nswindow && [keyPath isEqualToString:@"visible"]) { + int newVisibility = [[change objectForKey:@"new"] intValue]; + if (newVisibility) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_SHOWN, 0, 0); + } else if (![_data.nswindow isMiniaturized]) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIDDEN, 0, 0); + } + } +} + +- (void)pauseVisibleObservation +{ + observingVisible = NO; + wasVisible = [_data.nswindow isVisible]; +} + +- (void)resumeVisibleObservation +{ + BOOL isVisible = [_data.nswindow isVisible]; + observingVisible = YES; + if (wasVisible != isVisible) { + if (isVisible) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_SHOWN, 0, 0); + } else if (![_data.nswindow isMiniaturized]) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIDDEN, 0, 0); + } + + wasVisible = isVisible; + } +} + +- (BOOL)setFullscreenSpace:(BOOL)state +{ + SDL_Window *window = _data.window; + NSWindow *nswindow = _data.nswindow; + SDL_CocoaVideoData *videodata = ((__bridge SDL_CocoaWindowData *)window->internal).videodata; + + if (!videodata.allow_spaces) { + return NO; // Spaces are forcibly disabled. + } else if (state && window->fullscreen_exclusive) { + return NO; // we only allow you to make a Space on fullscreen desktop windows. + } else if (!state && window->last_fullscreen_exclusive_display) { + return NO; // we only handle leaving the Space on windows that were previously fullscreen desktop. + } else if (state == isFullscreenSpace) { + return YES; // already there. + } + + if (inFullscreenTransition) { + if (state) { + [self addPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN]; + } else { + [self addPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN]; + } + return YES; + } + inFullscreenTransition = YES; + + // you need to be FullScreenPrimary, or toggleFullScreen doesn't work. Unset it again in windowDidExitFullScreen. + [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + [nswindow performSelectorOnMainThread:@selector(toggleFullScreen:) withObject:nswindow waitUntilDone:NO]; + return YES; +} + +- (BOOL)isInFullscreenSpace +{ + return isFullscreenSpace; +} + +- (BOOL)isInFullscreenSpaceTransition +{ + return inFullscreenTransition; +} + +- (void)clearPendingWindowOperation:(PendingWindowOperation)operation +{ + pendingWindowOperation &= ~operation; +} + +- (void)addPendingWindowOperation:(PendingWindowOperation)operation +{ + pendingWindowOperation |= operation; +} + +- (BOOL)windowOperationIsPending:(PendingWindowOperation)operation +{ + return !!(pendingWindowOperation & operation); +} + +- (BOOL)hasPendingWindowOperation +{ + // A pending zoom may be deferred until leaving fullscreen, so don't block on it. + return (pendingWindowOperation & ~PENDING_OPERATION_ZOOM) != PENDING_OPERATION_NONE || + isMiniaturizing || inFullscreenTransition; +} + +- (void)close +{ + NSNotificationCenter *center; + NSWindow *window = _data.nswindow; + NSView *view = [window contentView]; + + center = [NSNotificationCenter defaultCenter]; + + if ([window delegate] != self) { + [center removeObserver:self name:NSWindowDidExposeNotification object:window]; + [center removeObserver:self name:NSWindowDidChangeOcclusionStateNotification object:window]; + [center removeObserver:self name:NSWindowWillStartLiveResizeNotification object:window]; + [center removeObserver:self name:NSWindowDidEndLiveResizeNotification object:window]; + [center removeObserver:self name:NSWindowWillMoveNotification object:window]; + [center removeObserver:self name:NSWindowDidMoveNotification object:window]; + [center removeObserver:self name:NSWindowDidResizeNotification object:window]; + [center removeObserver:self name:NSWindowWillMiniaturizeNotification object:window]; + [center removeObserver:self name:NSWindowDidMiniaturizeNotification object:window]; + [center removeObserver:self name:NSWindowDidDeminiaturizeNotification object:window]; + [center removeObserver:self name:NSWindowDidBecomeKeyNotification object:window]; + [center removeObserver:self name:NSWindowDidResignKeyNotification object:window]; + [center removeObserver:self name:NSWindowDidChangeBackingPropertiesNotification object:window]; + [center removeObserver:self name:NSWindowDidChangeScreenProfileNotification object:window]; + [center removeObserver:self name:NSWindowDidChangeScreenNotification object:window]; + [center removeObserver:self name:NSWindowWillEnterFullScreenNotification object:window]; + [center removeObserver:self name:NSWindowDidEnterFullScreenNotification object:window]; + [center removeObserver:self name:NSWindowWillExitFullScreenNotification object:window]; + [center removeObserver:self name:NSWindowDidExitFullScreenNotification object:window]; + [center removeObserver:self name:@"NSWindowDidFailToEnterFullScreenNotification" object:window]; + [center removeObserver:self name:@"NSWindowDidFailToExitFullScreenNotification" object:window]; + } else { + [window setDelegate:nil]; + } + + [window removeObserver:self forKeyPath:@"visible"]; + + if ([window nextResponder] == self) { + [window setNextResponder:nil]; + } + if ([view nextResponder] == self) { + [view setNextResponder:nil]; + } +} + +- (BOOL)isMoving +{ + return isMoving; +} + +- (BOOL)isMovingOrFocusClickPending +{ + return isMoving || (focusClickPending != 0); +} + +- (void)setFocusClickPending:(NSInteger)button +{ + focusClickPending |= (1 << button); +} + +- (void)clearFocusClickPending:(NSInteger)button +{ + if (focusClickPending & (1 << button)) { + focusClickPending &= ~(1 << button); + if (focusClickPending == 0) { + [self onMovingOrFocusClickPendingStateCleared]; + } + } +} + +- (void)updateIgnoreMouseState:(NSEvent *)theEvent +{ + SDL_Window *window = _data.window; + SDL_Surface *shape = (SDL_Surface *)SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_SHAPE_POINTER, NULL); + BOOL ignoresMouseEvents = NO; + + if (shape) { + NSPoint point = [theEvent locationInWindow]; + NSRect windowRect = [[_data.nswindow contentView] frame]; + if (NSMouseInRect(point, windowRect, NO)) { + int x = (int)SDL_roundf((point.x / (window->w - 1)) * (shape->w - 1)); + int y = (int)SDL_roundf(((window->h - point.y) / (window->h - 1)) * (shape->h - 1)); + Uint8 a; + + if (!SDL_ReadSurfacePixel(shape, x, y, NULL, NULL, NULL, &a) || a == SDL_ALPHA_TRANSPARENT) { + ignoresMouseEvents = YES; + } + } + } + _data.nswindow.ignoresMouseEvents = ignoresMouseEvents; +} + +- (void)setPendingMoveX:(float)x Y:(float)y +{ + pendingWindowWarpX = x; + pendingWindowWarpY = y; +} + +- (void)windowDidFinishMoving +{ + if (isMoving) { + isMoving = NO; + [self onMovingOrFocusClickPendingStateCleared]; + } +} + +- (void)onMovingOrFocusClickPendingStateCleared +{ + if (![self isMovingOrFocusClickPending]) { + SDL_Mouse *mouse = SDL_GetMouse(); + if (pendingWindowWarpX != FLT_MAX && pendingWindowWarpY != FLT_MAX) { + mouse->WarpMouseGlobal(pendingWindowWarpX, pendingWindowWarpY); + pendingWindowWarpX = pendingWindowWarpY = FLT_MAX; + } + if (mouse->relative_mode && mouse->focus == _data.window) { + // Move the cursor to the nearest point in the window + { + float x, y; + CGPoint cgpoint; + + SDL_GetMouseState(&x, &y); + cgpoint.x = _data.window->x + x; + cgpoint.y = _data.window->y + y; + + Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y); + + DLog("Returning cursor to (%g, %g)", cgpoint.x, cgpoint.y); + CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint); + } + + mouse->SetRelativeMouseMode(true); + } else { + Cocoa_UpdateClipCursor(_data.window); + } + } +} + +- (BOOL)windowShouldClose:(id)sender +{ + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_CLOSE_REQUESTED, 0, 0); + return NO; +} + +- (void)windowDidExpose:(NSNotification *)aNotification +{ + Cocoa_SendExposedEventIfVisible(_data.window); +} + +- (void)windowDidChangeOcclusionState:(NSNotification *)aNotification +{ + if ([_data.nswindow occlusionState] & NSWindowOcclusionStateVisible) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_EXPOSED, 0, 0); + } else { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_OCCLUDED, 0, 0); + } +} + +- (void)windowWillStartLiveResize:(NSNotification *)aNotification +{ + // We'll try to maintain 60 FPS during live resizing + const NSTimeInterval interval = 1.0 / 60.0; + liveResizeTimer = [NSTimer scheduledTimerWithTimeInterval:interval + repeats:TRUE + block:^(NSTimer *unusedTimer) + { + SDL_OnWindowLiveResizeUpdate(_data.window); + }]; + + [[NSRunLoop currentRunLoop] addTimer:liveResizeTimer forMode:NSRunLoopCommonModes]; +} + +- (void)windowDidEndLiveResize:(NSNotification *)aNotification +{ + [liveResizeTimer invalidate]; + liveResizeTimer = nil; +} + +- (void)windowWillMove:(NSNotification *)aNotification +{ + if ([_data.nswindow isKindOfClass:[SDL3Window class]]) { + pendingWindowWarpX = pendingWindowWarpY = FLT_MAX; + isMoving = YES; + } +} + +- (void)windowDidMove:(NSNotification *)aNotification +{ + int x, y; + SDL_Window *window = _data.window; + NSWindow *nswindow = _data.nswindow; + NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]]; + ConvertNSRect(&rect); + + if (inFullscreenTransition || b_inModeTransition) { + // We'll take care of this at the end of the transition + return; + } + + x = (int)rect.origin.x; + y = (int)rect.origin.y; + + ScheduleContextUpdates(_data); + + // Get the parent-relative coordinates for child windows. + SDL_GlobalToRelativeForWindow(window, x, y, &x, &y); + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MOVED, x, y); +} + +- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize +{ + SDL_Window *window = _data.window; + + if (window->min_aspect != window->max_aspect) { + NSWindow *nswindow = _data.nswindow; + NSRect newContentRect = [nswindow contentRectForFrameRect:NSMakeRect(0, 0, frameSize.width, frameSize.height)]; + NSSize newSize = newContentRect.size; + CGFloat minAspectRatio = window->min_aspect; + CGFloat maxAspectRatio = window->max_aspect; + CGFloat aspectRatio; + + if (newSize.height > 0) { + aspectRatio = newSize.width / newSize.height; + + if (maxAspectRatio > 0.0f && aspectRatio > maxAspectRatio) { + newSize.width = SDL_roundf(newSize.height * maxAspectRatio); + } else if (minAspectRatio > 0.0f && aspectRatio < minAspectRatio) { + newSize.height = SDL_roundf(newSize.width / minAspectRatio); + } + + NSRect newFrameRect = [nswindow frameRectForContentRect:NSMakeRect(0, 0, newSize.width, newSize.height)]; + frameSize = newFrameRect.size; + } + } + return frameSize; +} + +- (void)windowDidResize:(NSNotification *)aNotification +{ + SDL_Window *window; + NSWindow *nswindow; + NSRect rect; + int x, y, w, h; + BOOL zoomed; + + if (inFullscreenTransition || b_inModeTransition) { + // We'll take care of this at the end of the transition + return; + } + + if (focusClickPending) { + focusClickPending = 0; + [self onMovingOrFocusClickPendingStateCleared]; + } + window = _data.window; + nswindow = _data.nswindow; + rect = [nswindow contentRectForFrameRect:[nswindow frame]]; + ConvertNSRect(&rect); + x = (int)rect.origin.x; + y = (int)rect.origin.y; + w = (int)rect.size.width; + h = (int)rect.size.height; + + ScheduleContextUpdates(_data); + + /* isZoomed always returns true if the window is not resizable + * and fullscreen windows are considered zoomed. + */ + if ((window->flags & SDL_WINDOW_RESIZABLE) && [nswindow isZoomed] && + !(window->flags & SDL_WINDOW_FULLSCREEN) && ![self isInFullscreenSpace]) { + zoomed = YES; + } else { + zoomed = NO; + } + if (!zoomed) { + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESTORED, 0, 0); + } else { + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MAXIMIZED, 0, 0); + if ([self windowOperationIsPending:PENDING_OPERATION_MINIMIZE]) { + [nswindow miniaturize:nil]; + } + } + + /* The window can move during a resize event, such as when maximizing + or resizing from a corner */ + SDL_GlobalToRelativeForWindow(window, x, y, &x, &y); + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_MOVED, x, y); + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, w, h); +} + +- (void)windowWillMiniaturize:(NSNotification *)aNotification +{ + isMiniaturizing = YES; + Cocoa_WaitForMiniaturizable(_data.window); +} + +- (void)windowDidMiniaturize:(NSNotification *)aNotification +{ + if (focusClickPending) { + focusClickPending = 0; + [self onMovingOrFocusClickPendingStateCleared]; + } + isMiniaturizing = NO; + [self clearPendingWindowOperation:PENDING_OPERATION_MINIMIZE]; + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_MINIMIZED, 0, 0); +} + +- (void)windowDidDeminiaturize:(NSNotification *)aNotification +{ + // Always send restored before maximized. + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_RESTORED, 0, 0); + + if (Cocoa_IsWindowZoomed(_data.window)) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_MAXIMIZED, 0, 0); + } + + if ([self windowOperationIsPending:PENDING_OPERATION_ENTER_FULLSCREEN]) { + SDL_UpdateFullscreenMode(_data.window, true, true); + } +} + +- (void)windowDidBecomeKey:(NSNotification *)aNotification +{ + SDL_Window *window = _data.window; + + // We're going to get keyboard events, since we're key. + // This needs to be done before restoring the relative mouse mode. + Cocoa_SetKeyboardFocus(_data.keyboard_focus ? _data.keyboard_focus : window, true); + + // If we just gained focus we need the updated mouse position + if (!(window->flags & SDL_WINDOW_MOUSE_RELATIVE_MODE)) { + NSPoint point; + float x, y; + + point = [_data.nswindow mouseLocationOutsideOfEventStream]; + x = point.x; + y = (window->h - point.y); + + if (x >= 0.0f && x < (float)window->w && y >= 0.0f && y < (float)window->h) { + SDL_SendMouseMotion(0, window, SDL_GLOBAL_MOUSE_ID, false, x, y); + } + } + + // Check to see if someone updated the clipboard + Cocoa_CheckClipboardUpdate(_data.videodata); + + if (isFullscreenSpace && !window->fullscreen_exclusive) { + Cocoa_ToggleFullscreenSpaceMenuVisibility(window); + } + { + const unsigned int newflags = [NSEvent modifierFlags] & NSEventModifierFlagCapsLock; + _data.videodata.modifierFlags = (_data.videodata.modifierFlags & ~NSEventModifierFlagCapsLock) | newflags; + SDL_ToggleModState(SDL_KMOD_CAPS, newflags ? true : false); + } + + /* Restore fullscreen mode unless the window is deminiaturizing. + * If it is, fullscreen will be restored when deminiaturization is complete. + */ + if (!(window->flags & SDL_WINDOW_MINIMIZED) && + [self windowOperationIsPending:PENDING_OPERATION_ENTER_FULLSCREEN]) { + SDL_UpdateFullscreenMode(window, true, true); + } +} + +- (void)windowDidResignKey:(NSNotification *)aNotification +{ + // Some other window will get mouse events, since we're not key. + if (SDL_GetMouseFocus() == _data.window) { + SDL_SetMouseFocus(NULL); + } + + // Some other window will get keyboard events, since we're not key. + if (SDL_GetKeyboardFocus() == _data.window) { + SDL_SetKeyboardFocus(NULL); + } + + if (isFullscreenSpace) { + [NSMenu setMenuBarVisible:YES]; + } +} + +- (void)windowDidChangeBackingProperties:(NSNotification *)aNotification +{ + NSNumber *oldscale = [[aNotification userInfo] objectForKey:NSBackingPropertyOldScaleFactorKey]; + + if (inFullscreenTransition) { + return; + } + + if ([oldscale doubleValue] != [_data.nswindow backingScaleFactor]) { + // Send a resize event when the backing scale factor changes. + [self windowDidResize:aNotification]; + } +} + +- (void)windowDidChangeScreenProfile:(NSNotification *)aNotification +{ + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_ICCPROF_CHANGED, 0, 0); +} + +- (void)windowDidChangeScreen:(NSNotification *)aNotification +{ + // printf("WINDOWDIDCHANGESCREEN\n"); + +#ifdef SDL_VIDEO_OPENGL + + if (_data && _data.nscontexts) { + for (SDL3OpenGLContext *context in _data.nscontexts) { + [context movedToNewScreen]; + } + } + +#endif // SDL_VIDEO_OPENGL +} + +- (void)windowWillEnterFullScreen:(NSNotification *)aNotification +{ + SDL_Window *window = _data.window; + const NSUInteger flags = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable | NSWindowStyleMaskTitled; + + /* For some reason, the fullscreen window won't get any mouse button events + * without the NSWindowStyleMaskTitled flag being set when entering fullscreen, + * so it's needed even if the window is borderless. + */ + SetWindowStyle(window, flags); + + _data.was_zoomed = !!(window->flags & SDL_WINDOW_MAXIMIZED); + + isFullscreenSpace = YES; + inFullscreenTransition = YES; +} + +- (void)windowDidFailToEnterFullScreen:(NSNotification *)aNotification +{ + SDL_Window *window = _data.window; + + if (window->is_destroying) { + return; + } + + SetWindowStyle(window, GetWindowStyle(window)); + + [self clearPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN]; + isFullscreenSpace = NO; + inFullscreenTransition = NO; + + [self windowDidExitFullScreen:nil]; +} + +- (void)windowDidEnterFullScreen:(NSNotification *)aNotification +{ + SDL_Window *window = _data.window; + + inFullscreenTransition = NO; + [self clearPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN]; + + if ([self windowOperationIsPending:PENDING_OPERATION_LEAVE_FULLSCREEN]) { + [self setFullscreenSpace:NO]; + } else { + Cocoa_ToggleFullscreenSpaceMenuVisibility(window); + + /* Don't recurse back into UpdateFullscreenMode() if this was hit in + * a blocking transition, as the caller is already waiting in + * UpdateFullscreenMode(). + */ + if (!_data.in_blocking_transition) { + SDL_UpdateFullscreenMode(window, true, false); + } + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_ENTER_FULLSCREEN, 0, 0); + + _data.pending_position = NO; + _data.pending_size = NO; + + /* Force the size change event in case it was delivered earlier + while the window was still animating into place. + */ + window->w = 0; + window->h = 0; + [self windowDidMove:aNotification]; + [self windowDidResize:aNotification]; + + Cocoa_UpdateClipCursor(window); + } +} + +- (void)windowWillExitFullScreen:(NSNotification *)aNotification +{ + SDL_Window *window = _data.window; + + /* If the windowed mode borders were toggled on while in a fullscreen space, + * NSWindowStyleMaskTitled has to be cleared here, or the window can end up + * in a weird, semi-decorated state upon returning to windowed mode. + */ + if (_data.border_toggled && !(window->flags & SDL_WINDOW_BORDERLESS)) { + const NSUInteger flags = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + + SetWindowStyle(window, flags); + _data.border_toggled = false; + } + + isFullscreenSpace = NO; + inFullscreenTransition = YES; +} + +- (void)windowDidFailToExitFullScreen:(NSNotification *)aNotification +{ + SDL_Window *window = _data.window; + const NSUInteger flags = NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + + if (window->is_destroying) { + return; + } + + _data.pending_position = NO; + _data.pending_size = NO; + window->last_position_pending = false; + window->last_size_pending = false; + + SetWindowStyle(window, flags); + + isFullscreenSpace = YES; + inFullscreenTransition = NO; + + [self windowDidEnterFullScreen:nil]; +} + +- (void)windowDidExitFullScreen:(NSNotification *)aNotification +{ + SDL_Window *window = _data.window; + NSWindow *nswindow = _data.nswindow; + + inFullscreenTransition = NO; + _data.fullscreen_space_requested = NO; + + /* As of macOS 10.15, the window decorations can go missing sometimes after + certain fullscreen-desktop->exlusive-fullscreen->windowed mode flows + sometimes. Making sure the style mask always uses the windowed mode style + when returning to windowed mode from a space (instead of using a pending + fullscreen mode style mask) seems to work around that issue. + */ + SetWindowStyle(window, GetWindowWindowedStyle(window)); + + /* Don't recurse back into UpdateFullscreenMode() if this was hit in + * a blocking transition, as the caller is already waiting in + * UpdateFullscreenMode(). + */ + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0); + if (!_data.in_blocking_transition) { + SDL_UpdateFullscreenMode(window, false, false); + } + + if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) { + [nswindow setLevel:NSFloatingWindowLevel]; + } else { + [nswindow setLevel:kCGNormalWindowLevel]; + } + + [self clearPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN]; + + if ([self windowOperationIsPending:PENDING_OPERATION_ENTER_FULLSCREEN]) { + [self setFullscreenSpace:YES]; + } else if ([self windowOperationIsPending:PENDING_OPERATION_MINIMIZE]) { + /* There's some state that isn't quite back to normal when + * windowDidExitFullScreen triggers. For example, the minimize button on + * the title bar doesn't actually enable for another 200 milliseconds or + * so on this MacBook. Camp here and wait for that to happen before + * going on, in case we're exiting fullscreen to minimize, which need + * that window state to be normal before it will work. + */ + Cocoa_WaitForMiniaturizable(_data.window); + [self addPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN]; + [nswindow miniaturize:nil]; + } else { + // Adjust the fullscreen toggle button and readd menu now that we're here. + if (window->flags & SDL_WINDOW_RESIZABLE) { + // resizable windows are Spaces-friendly: they get the "go fullscreen" toggle button on their titlebar. + [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + } else { + [nswindow setCollectionBehavior:NSWindowCollectionBehaviorManaged]; + } + [NSMenu setMenuBarVisible:YES]; + + // Toggle zoom, if changed while fullscreen. + if ([self windowOperationIsPending:PENDING_OPERATION_ZOOM]) { + [self clearPendingWindowOperation:PENDING_OPERATION_ZOOM]; + [nswindow zoom:nil]; + } + + if (![nswindow isZoomed]) { + // Apply a pending window size, if not zoomed. + NSRect rect; + rect.origin.x = _data.pending_position ? window->pending.x : window->floating.x; + rect.origin.y = _data.pending_position ? window->pending.y : window->floating.y; + rect.size.width = _data.pending_size ? window->pending.w : window->floating.w; + rect.size.height = _data.pending_size ? window->pending.h : window->floating.h; + ConvertNSRect(&rect); + + if (_data.pending_size) { + [nswindow setContentSize:rect.size]; + } + if (_data.pending_position) { + [nswindow setFrameOrigin:rect.origin]; + } + } + + _data.pending_size = NO; + _data.pending_position = NO; + _data.was_zoomed = NO; + + /* Force the size change event in case it was delivered earlier + * while the window was still animating into place. + */ + window->w = 0; + window->h = 0; + [self windowDidMove:aNotification]; + [self windowDidResize:aNotification]; + + // FIXME: Why does the window get hidden? + if (!(window->flags & SDL_WINDOW_HIDDEN)) { + Cocoa_ShowWindow(SDL_GetVideoDevice(), window); + } + + Cocoa_UpdateClipCursor(window); + } +} + +- (NSApplicationPresentationOptions)window:(NSWindow *)window willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions +{ + if (_data.window->fullscreen_exclusive) { + return NSApplicationPresentationFullScreen | NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar; + } else { + return proposedOptions; + } +} + +/* We'll respond to key events by mostly doing nothing so we don't beep. + * We could handle key messages here, but we lose some in the NSApp dispatch, + * where they get converted to action messages, etc. + */ +- (void)flagsChanged:(NSEvent *)theEvent +{ + // Cocoa_HandleKeyEvent(SDL_GetVideoDevice(), theEvent); + + /* Catch capslock in here as a special case: + https://developer.apple.com/library/archive/qa/qa1519/_index.html + Note that technote's check of keyCode doesn't work. At least on the + 10.15 beta, capslock comes through here as keycode 255, but it's safe + to send duplicate key events; SDL filters them out quickly in + SDL_SendKeyboardKey(). */ + + /* Also note that SDL_SendKeyboardKey expects all capslock events to be + keypresses; it won't toggle the mod state if you send a keyrelease. */ + const bool osenabled = ([theEvent modifierFlags] & NSEventModifierFlagCapsLock) ? true : false; + const bool sdlenabled = (SDL_GetModState() & SDL_KMOD_CAPS) ? true : false; + if (osenabled ^ sdlenabled) { + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_CAPSLOCK, true); + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, SDL_SCANCODE_CAPSLOCK, false); + } +} +- (void)keyDown:(NSEvent *)theEvent +{ + // Cocoa_HandleKeyEvent(SDL_GetVideoDevice(), theEvent); +} +- (void)keyUp:(NSEvent *)theEvent +{ + // Cocoa_HandleKeyEvent(SDL_GetVideoDevice(), theEvent); +} + +/* We'll respond to selectors by doing nothing so we don't beep. + * The escape key gets converted to a "cancel" selector, etc. + */ +- (void)doCommandBySelector:(SEL)aSelector +{ + // NSLog(@"doCommandBySelector: %@\n", NSStringFromSelector(aSelector)); +} + +- (void)updateHitTest +{ + SDL_Window *window = _data.window; + BOOL draggable = NO; + + if (window->hit_test) { + float x, y; + SDL_Point point; + + SDL_GetGlobalMouseState(&x, &y); + point.x = (int)SDL_roundf(x - window->x); + point.y = (int)SDL_roundf(y - window->y); + if (point.x >= 0 && point.x < window->w && point.y >= 0 && point.y < window->h) { + if (window->hit_test(window, &point, window->hit_test_data) == SDL_HITTEST_DRAGGABLE) { + draggable = YES; + } + } + } + + if (isDragAreaRunning != draggable) { + isDragAreaRunning = draggable; + [_data.nswindow setMovableByWindowBackground:draggable]; + } +} + +- (BOOL)processHitTest:(NSEvent *)theEvent +{ + SDL_Window *window = _data.window; + + if (window->hit_test) { // if no hit-test, skip this. + const NSPoint location = [theEvent locationInWindow]; + const SDL_Point point = { (int)location.x, window->h - (((int)location.y) - 1) }; + const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data); + if (rc == SDL_HITTEST_DRAGGABLE) { + if (!isDragAreaRunning) { + isDragAreaRunning = YES; + [_data.nswindow setMovableByWindowBackground:YES]; + } + return YES; // dragging! + } else { + if (isDragAreaRunning) { + isDragAreaRunning = NO; + [_data.nswindow setMovableByWindowBackground:NO]; + return YES; // was dragging, drop event. + } + } + } + + return NO; // not a special area, carry on. +} + +static void Cocoa_SendMouseButtonClicks(SDL_Mouse *mouse, NSEvent *theEvent, SDL_Window *window, Uint8 button, bool down) +{ + SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID; + //const int clicks = (int)[theEvent clickCount]; + SDL_Window *focus = SDL_GetKeyboardFocus(); + + // macOS will send non-left clicks to background windows without raising them, so we need to + // temporarily adjust the mouse position when this happens, as `mouse` will be tracking + // the position in the currently-focused window. We don't (currently) send a mousemove + // event for the background window, this just makes sure the button is reported at the + // correct position in its own event. + if (focus && ([theEvent window] == ((__bridge SDL_CocoaWindowData *)focus->internal).nswindow)) { + //SDL_SendMouseButtonClicks(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down, clicks); + SDL_SendMouseButton(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down); + } else { + const float orig_x = mouse->x; + const float orig_y = mouse->y; + const NSPoint point = [theEvent locationInWindow]; + mouse->x = (int)point.x; + mouse->y = (int)(window->h - point.y); + //SDL_SendMouseButtonClicks(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down, clicks); + SDL_SendMouseButton(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, button, down); + mouse->x = orig_x; + mouse->y = orig_y; + } +} + +- (void)mouseDown:(NSEvent *)theEvent +{ + if (Cocoa_HandlePenEvent(_data, theEvent)) { + return; // pen code handled it. + } + + SDL_Mouse *mouse = SDL_GetMouse(); + int button; + + if (!mouse) { + return; + } + + // Ignore events that aren't inside the client area (i.e. title bar.) + if ([theEvent window]) { + NSRect windowRect = [[[theEvent window] contentView] frame]; + if (!NSMouseInRect([theEvent locationInWindow], windowRect, NO)) { + return; + } + } + + switch ([theEvent buttonNumber]) { + case 0: + if (([theEvent modifierFlags] & NSEventModifierFlagControl) && + GetHintCtrlClickEmulateRightClick()) { + wasCtrlLeft = YES; + button = SDL_BUTTON_RIGHT; + } else { + wasCtrlLeft = NO; + button = SDL_BUTTON_LEFT; + } + break; + case 1: + button = SDL_BUTTON_RIGHT; + break; + case 2: + button = SDL_BUTTON_MIDDLE; + break; + default: + button = (int)[theEvent buttonNumber] + 1; + break; + } + + if (button == SDL_BUTTON_LEFT && [self processHitTest:theEvent]) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0); + return; // dragging, drop event. + } + + Cocoa_SendMouseButtonClicks(mouse, theEvent, _data.window, button, true); +} + +- (void)rightMouseDown:(NSEvent *)theEvent +{ + [self mouseDown:theEvent]; +} + +- (void)otherMouseDown:(NSEvent *)theEvent +{ + [self mouseDown:theEvent]; +} + +- (void)mouseUp:(NSEvent *)theEvent +{ + if (Cocoa_HandlePenEvent(_data, theEvent)) { + return; // pen code handled it. + } + + SDL_Mouse *mouse = SDL_GetMouse(); + int button; + + if (!mouse) { + return; + } + + switch ([theEvent buttonNumber]) { + case 0: + if (wasCtrlLeft) { + button = SDL_BUTTON_RIGHT; + wasCtrlLeft = NO; + } else { + button = SDL_BUTTON_LEFT; + } + break; + case 1: + button = SDL_BUTTON_RIGHT; + break; + case 2: + button = SDL_BUTTON_MIDDLE; + break; + default: + button = (int)[theEvent buttonNumber] + 1; + break; + } + + if (button == SDL_BUTTON_LEFT && [self processHitTest:theEvent]) { + SDL_SendWindowEvent(_data.window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0); + return; // stopped dragging, drop event. + } + + Cocoa_SendMouseButtonClicks(mouse, theEvent, _data.window, button, false); +} + +- (void)rightMouseUp:(NSEvent *)theEvent +{ + [self mouseUp:theEvent]; +} + +- (void)otherMouseUp:(NSEvent *)theEvent +{ + [self mouseUp:theEvent]; +} + +- (void)mouseMoved:(NSEvent *)theEvent +{ + if (Cocoa_HandlePenEvent(_data, theEvent)) { + return; // pen code handled it. + } + + SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID; + SDL_Mouse *mouse = SDL_GetMouse(); + NSPoint point; + float x, y; + SDL_Window *window; + NSView *contentView; + + if (!mouse) { + return; + } + + if (!Cocoa_GetMouseFocus()) { + // The mouse is no longer over any window in the application + SDL_SetMouseFocus(NULL); + return; + } + + window = _data.window; + contentView = _data.sdlContentView; + point = [theEvent locationInWindow]; + + if ([contentView mouse:[contentView convertPoint:point fromView:nil] inRect:[contentView bounds]] && + [NSCursor currentCursor] != Cocoa_GetDesiredCursor()) { + // The wrong cursor is on screen, fix it. This fixes an macOS bug that is only known to + // occur in fullscreen windows on the built-in displays of newer MacBooks with camera + // notches. When the mouse is moved near the top of such a window (within about 44 units) + // and then moved back down, the cursor rects aren't respected. + [_data.nswindow invalidateCursorRectsForView:contentView]; + } + + if (window->flags & SDL_WINDOW_TRANSPARENT) { + [self updateIgnoreMouseState:theEvent]; + } + + if ([self processHitTest:theEvent]) { + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0); + return; // dragging, drop event. + } + + if (mouse->relative_mode) { + return; + } + + x = point.x; + y = (window->h - point.y); + + if (NSAppKitVersionNumber >= NSAppKitVersionNumber10_13_2) { + // Mouse grab is taken care of by the confinement rect + } else { + CGPoint cgpoint; + if (ShouldAdjustCoordinatesForGrab(window) && + AdjustCoordinatesForGrab(window, window->x + x, window->y + y, &cgpoint)) { + Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y); + CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint); + CGAssociateMouseAndMouseCursorPosition(YES); + } + } + + SDL_SendMouseMotion(Cocoa_GetEventTimestamp([theEvent timestamp]), window, mouseID, false, x, y); +} + +- (void)mouseDragged:(NSEvent *)theEvent +{ + [self mouseMoved:theEvent]; +} + +- (void)rightMouseDragged:(NSEvent *)theEvent +{ + [self mouseMoved:theEvent]; +} + +- (void)otherMouseDragged:(NSEvent *)theEvent +{ + [self mouseMoved:theEvent]; +} + +- (void)scrollWheel:(NSEvent *)theEvent +{ + Cocoa_HandleMouseWheel(_data.window, theEvent); +} + +- (BOOL)isTouchFromTrackpad:(NSEvent *)theEvent +{ + SDL_Window *window = _data.window; + SDL_CocoaVideoData *videodata = ((__bridge SDL_CocoaWindowData *)window->internal).videodata; + + /* if this a MacBook trackpad, we'll make input look like a synthesized + event. This is backwards from reality, but better matches user + expectations. You can make it look like a generic touch device instead + with the SDL_HINT_TRACKPAD_IS_TOUCH_ONLY hint. */ + BOOL istrackpad = NO; + if (!videodata.trackpad_is_touch_only) { + @try { + istrackpad = ([theEvent subtype] == NSEventSubtypeMouseEvent); + } + @catch (NSException *e) { + /* if NSEvent type doesn't have subtype, such as NSEventTypeBeginGesture on + * macOS 10.5 to 10.10, then NSInternalInconsistencyException is thrown. + * This still prints a message to terminal so catching it's not an ideal solution. + * + * *** Assertion failure in -[NSEvent subtype] + */ + } + } + return istrackpad; +} + +- (void)touchesBeganWithEvent:(NSEvent *)theEvent +{ + NSSet *touches; + SDL_TouchID touchID; + int existingTouchCount; + const BOOL istrackpad = [self isTouchFromTrackpad:theEvent]; + + touches = [theEvent touchesMatchingPhase:NSTouchPhaseAny inView:nil]; + touchID = istrackpad ? SDL_MOUSE_TOUCHID : (SDL_TouchID)(intptr_t)[[touches anyObject] device]; + existingTouchCount = 0; + + for (NSTouch *touch in touches) { + if ([touch phase] != NSTouchPhaseBegan) { + existingTouchCount++; + } + } + if (existingTouchCount == 0) { + int numFingers; + SDL_Finger **fingers = SDL_GetTouchFingers(touchID, &numFingers); + if (fingers) { + DLog("Reset Lost Fingers: %d", numFingers); + for (--numFingers; numFingers >= 0; --numFingers) { + const SDL_Finger *finger = fingers[numFingers]; + /* trackpad touches have no window. If we really wanted one we could + * use the window that has mouse or keyboard focus. + * Sending a null window currently also prevents synthetic mouse + * events from being generated from touch events. + */ + SDL_Window *window = NULL; + SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchID, finger->id, window, SDL_EVENT_FINGER_CANCELED, 0, 0, 0); + } + SDL_free(fingers); + } + } + + DLog("Began Fingers: %lu .. existing: %d", (unsigned long)[touches count], existingTouchCount); + [self handleTouches:NSTouchPhaseBegan withEvent:theEvent]; +} + +- (void)touchesMovedWithEvent:(NSEvent *)theEvent +{ + [self handleTouches:NSTouchPhaseMoved withEvent:theEvent]; +} + +- (void)touchesEndedWithEvent:(NSEvent *)theEvent +{ + [self handleTouches:NSTouchPhaseEnded withEvent:theEvent]; +} + +- (void)touchesCancelledWithEvent:(NSEvent *)theEvent +{ + [self handleTouches:NSTouchPhaseCancelled withEvent:theEvent]; +} + +- (void)handleTouches:(NSTouchPhase)phase withEvent:(NSEvent *)theEvent +{ + NSSet *touches = [theEvent touchesMatchingPhase:phase inView:nil]; + const BOOL istrackpad = [self isTouchFromTrackpad:theEvent]; + SDL_FingerID fingerId; + float x, y; + + for (NSTouch *touch in touches) { + const SDL_TouchID touchId = istrackpad ? SDL_MOUSE_TOUCHID : (SDL_TouchID)(uintptr_t)[touch device]; + SDL_TouchDeviceType devtype = SDL_TOUCH_DEVICE_INDIRECT_ABSOLUTE; + + /* trackpad touches have no window. If we really wanted one we could + * use the window that has mouse or keyboard focus. + * Sending a null window currently also prevents synthetic mouse events + * from being generated from touch events. + */ + SDL_Window *window = NULL; + + /* TODO: Before implementing direct touch support here, we need to + * figure out whether the OS generates mouse events from them on its + * own. If it does, we should prevent SendTouch from generating + * synthetic mouse events for these touches itself (while also + * sending a window.) It will also need to use normalized window- + * relative coordinates via [touch locationInView:]. + */ + if ([touch type] == NSTouchTypeDirect) { + continue; + } + + if (SDL_AddTouch(touchId, devtype, "") < 0) { + return; + } + + fingerId = (SDL_FingerID)(uintptr_t)[touch identity]; + x = [touch normalizedPosition].x; + y = [touch normalizedPosition].y; + // Make the origin the upper left instead of the lower left + y = 1.0f - y; + + switch (phase) { + case NSTouchPhaseBegan: + SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, SDL_EVENT_FINGER_DOWN, x, y, 1.0f); + break; + case NSTouchPhaseEnded: + SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, SDL_EVENT_FINGER_UP, x, y, 1.0f); + break; + case NSTouchPhaseCancelled: + SDL_SendTouch(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, SDL_EVENT_FINGER_CANCELED, x, y, 1.0f); + break; + case NSTouchPhaseMoved: + SDL_SendTouchMotion(Cocoa_GetEventTimestamp([theEvent timestamp]), touchId, fingerId, window, x, y, 1.0f); + break; + default: + break; + } + } +} + +- (void)tabletProximity:(NSEvent *)theEvent +{ + Cocoa_HandlePenEvent(_data, theEvent); +} + +- (void)tabletPoint:(NSEvent *)theEvent +{ + Cocoa_HandlePenEvent(_data, theEvent); +} + +@end + +@interface SDL3View : NSView +{ + SDL_Window *_sdlWindow; +} + +- (void)setSDLWindow:(SDL_Window *)window; + +// The default implementation doesn't pass rightMouseDown to responder chain +- (void)rightMouseDown:(NSEvent *)theEvent; +- (BOOL)mouseDownCanMoveWindow; +- (void)drawRect:(NSRect)dirtyRect; +- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent; +- (BOOL)wantsUpdateLayer; +- (void)updateLayer; +@end + +@implementation SDL3View + +- (void)setSDLWindow:(SDL_Window *)window +{ + _sdlWindow = window; +} + +/* this is used on older macOS revisions, and newer ones which emulate old + NSOpenGLContext behaviour while still using a layer under the hood. 10.8 and + later use updateLayer, up until 10.14.2 or so, which uses drawRect without + a GraphicsContext and with a layer active instead (for OpenGL contexts). */ +- (void)drawRect:(NSRect)dirtyRect +{ + /* Force the graphics context to clear to black so we don't get a flash of + white until the app is ready to draw. In practice on modern macOS, this + only gets called for window creation and other extraordinary events. */ + BOOL transparent = (_sdlWindow->flags & SDL_WINDOW_TRANSPARENT) != 0; + if ([NSGraphicsContext currentContext]) { + NSColor *fillColor = transparent ? [NSColor clearColor] : [NSColor blackColor]; + [fillColor setFill]; + NSRectFill(dirtyRect); + } else if (self.layer) { + CFStringRef color = transparent ? kCGColorClear : kCGColorBlack; + self.layer.backgroundColor = CGColorGetConstantColor(color); + } + + Cocoa_SendExposedEventIfVisible(_sdlWindow); +} + +- (BOOL)wantsUpdateLayer +{ + return YES; +} + +// This is also called when a Metal layer is active. +- (void)updateLayer +{ + /* Force the graphics context to clear to black so we don't get a flash of + white until the app is ready to draw. In practice on modern macOS, this + only gets called for window creation and other extraordinary events. */ + BOOL transparent = (_sdlWindow->flags & SDL_WINDOW_TRANSPARENT) != 0; + CFStringRef color = transparent ? kCGColorClear : kCGColorBlack; + self.layer.backgroundColor = CGColorGetConstantColor(color); + ScheduleContextUpdates((__bridge SDL_CocoaWindowData *)_sdlWindow->internal); + Cocoa_SendExposedEventIfVisible(_sdlWindow); +} + +- (void)rightMouseDown:(NSEvent *)theEvent +{ + [[self nextResponder] rightMouseDown:theEvent]; +} + +- (BOOL)mouseDownCanMoveWindow +{ + /* Always say YES, but this doesn't do anything until we call + -[NSWindow setMovableByWindowBackground:YES], which we ninja-toggle + during mouse events when we're using a drag area. */ + return YES; +} + +- (void)resetCursorRects +{ + [super resetCursorRects]; + [self addCursorRect:[self bounds] + cursor:Cocoa_GetDesiredCursor()]; +} + +- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent +{ + if (SDL_GetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH)) { + return SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, false); + } else { + return SDL_GetHintBoolean("SDL_MAC_MOUSE_FOCUS_CLICKTHROUGH", false); + } +} + +@end + +static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, NSWindow *nswindow, NSView *nsview) +{ + @autoreleasepool { + SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal; + SDL_CocoaWindowData *data; + + // Allocate the window data + data = [[SDL_CocoaWindowData alloc] init]; + if (!data) { + return SDL_OutOfMemory(); + } + data.window = window; + data.nswindow = nswindow; + data.videodata = videodata; + data.window_number = nswindow.windowNumber; + data.nscontexts = [[NSMutableArray alloc] init]; + data.sdlContentView = nsview; + + // Create an event listener for the window + data.listener = [[SDL3Cocoa_WindowListener alloc] init]; + + // Fill in the SDL window with the window data + { + int x, y; + NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]]; + ConvertNSRect(&rect); + SDL_GlobalToRelativeForWindow(window, (int)rect.origin.x, (int)rect.origin.y, &x, &y); + window->x = x; + window->y = y; + window->w = (int)rect.size.width; + window->h = (int)rect.size.height; + } + + // Set up the listener after we create the view + [data.listener listen:data]; + + if ([nswindow isVisible]) { + window->flags &= ~SDL_WINDOW_HIDDEN; + } else { + window->flags |= SDL_WINDOW_HIDDEN; + } + + { + unsigned long style = [nswindow styleMask]; + + /* NSWindowStyleMaskBorderless is zero, and it's possible to be + Resizeable _and_ borderless, so we can't do a simple bitwise AND + of NSWindowStyleMaskBorderless here. */ + if ((style & ~(NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable)) == NSWindowStyleMaskBorderless) { + window->flags |= SDL_WINDOW_BORDERLESS; + } else { + window->flags &= ~SDL_WINDOW_BORDERLESS; + } + if (style & NSWindowStyleMaskResizable) { + window->flags |= SDL_WINDOW_RESIZABLE; + } else { + window->flags &= ~SDL_WINDOW_RESIZABLE; + } + } + + // isZoomed always returns true if the window is not resizable + if ((window->flags & SDL_WINDOW_RESIZABLE) && [nswindow isZoomed]) { + window->flags |= SDL_WINDOW_MAXIMIZED; + } else { + window->flags &= ~SDL_WINDOW_MAXIMIZED; + } + + if ([nswindow isMiniaturized]) { + window->flags |= SDL_WINDOW_MINIMIZED; + } else { + window->flags &= ~SDL_WINDOW_MINIMIZED; + } + + if (window->parent) { + NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow; + [nsparent addChildWindow:nswindow ordered:NSWindowAbove]; + + /* FIXME: Should not need to call addChildWindow then orderOut. + Attaching a hidden child window to a hidden parent window will cause the child window + to show when the parent does. We therefore shouldn't attach the child window here as we're + going to do so when the child window is explicitly shown later but skipping the addChildWindow + entirely causes the child window to not get key focus correctly the first time it's shown. Adding + then immediately ordering out (removing) the window does work. */ + if (window->flags & SDL_WINDOW_HIDDEN) { + [nswindow orderOut:nil]; + } + } + + if (!SDL_WINDOW_IS_POPUP(window)) { + if ([nswindow isKeyWindow]) { + window->flags |= SDL_WINDOW_INPUT_FOCUS; + Cocoa_SetKeyboardFocus(data.window, true); + } + } else { + if (window->flags & SDL_WINDOW_TOOLTIP) { + [nswindow setIgnoresMouseEvents:YES]; + } else if (window->flags & SDL_WINDOW_POPUP_MENU) { + Cocoa_SetKeyboardFocus(window, window->parent == SDL_GetKeyboardFocus()); + } + } + + if (nswindow.isOpaque) { + window->flags &= ~SDL_WINDOW_TRANSPARENT; + } else { + window->flags |= SDL_WINDOW_TRANSPARENT; + } + + /* SDL_CocoaWindowData will be holding a strong reference to the NSWindow, and + * it will also call [NSWindow close] in DestroyWindow before releasing the + * NSWindow, so the extra release provided by releasedWhenClosed isn't + * necessary. */ + nswindow.releasedWhenClosed = NO; + + /* Prevents the window's "window device" from being destroyed when it is + * hidden. See http://www.mikeash.com/pyblog/nsopenglcontext-and-one-shot.html + */ + [nswindow setOneShot:NO]; + + if (window->flags & SDL_WINDOW_EXTERNAL) { + // Query the title from the existing window + NSString *title = [nswindow title]; + if (title) { + window->title = SDL_strdup([title UTF8String]); + } + } + + SDL_PropertiesID props = SDL_GetWindowProperties(window); + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, (__bridge void *)data.nswindow); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_COCOA_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG); + + // All done! + window->internal = (SDL_WindowData *)CFBridgingRetain(data); + return true; + } +} + +bool Cocoa_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) +{ + @autoreleasepool { + SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal; + const void *data = SDL_GetPointerProperty(create_props, "sdl2-compat.external_window", NULL); + NSWindow *nswindow = nil; + NSView *nsview = nil; + + if (data) { + if ([(__bridge id)data isKindOfClass:[NSWindow class]]) { + nswindow = (__bridge NSWindow *)data; + } else if ([(__bridge id)data isKindOfClass:[NSView class]]) { + nsview = (__bridge NSView *)data; + } else { + SDL_assert(false); + } + } else { + nswindow = (__bridge NSWindow *)SDL_GetPointerProperty(create_props, SDL_PROP_WINDOW_CREATE_COCOA_WINDOW_POINTER, NULL); + nsview = (__bridge NSView *)SDL_GetPointerProperty(create_props, SDL_PROP_WINDOW_CREATE_COCOA_VIEW_POINTER, NULL); + } + if (nswindow && !nsview) { + nsview = [nswindow contentView]; + } + if (nsview && !nswindow) { + nswindow = [nsview window]; + } + if (nswindow) { + window->flags |= SDL_WINDOW_EXTERNAL; + } else { + int x, y; + NSScreen *screen; + NSRect rect, screenRect; + NSUInteger style; + SDL3View *contentView; + + SDL_RelativeToGlobalForWindow(window, window->x, window->y, &x, &y); + rect.origin.x = x; + rect.origin.y = y; + rect.size.width = window->w; + rect.size.height = window->h; + ConvertNSRect(&rect); + + style = GetWindowStyle(window); + + // Figure out which screen to place this window + screen = ScreenForRect(&rect); + screenRect = [screen frame]; + rect.origin.x -= screenRect.origin.x; + rect.origin.y -= screenRect.origin.y; + + // Constrain the popup + if (SDL_WINDOW_IS_POPUP(window)) { + if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) { + rect.origin.x -= (rect.origin.x + rect.size.width) - (screenRect.origin.x + screenRect.size.width); + } + if (rect.origin.y + rect.size.height > screenRect.origin.y + screenRect.size.height) { + rect.origin.y -= (rect.origin.y + rect.size.height) - (screenRect.origin.y + screenRect.size.height); + } + rect.origin.x = SDL_max(rect.origin.x, screenRect.origin.x); + rect.origin.y = SDL_max(rect.origin.y, screenRect.origin.y); + } + + @try { + nswindow = [[SDL3Window alloc] initWithContentRect:rect styleMask:style backing:NSBackingStoreBuffered defer:NO screen:screen]; + } + @catch (NSException *e) { + return SDL_SetError("%s", [[e reason] UTF8String]); + } + + [nswindow setColorSpace:[NSColorSpace sRGBColorSpace]]; + + [nswindow setTabbingMode:NSWindowTabbingModeDisallowed]; + + if (videodata.allow_spaces) { + // we put fullscreen desktop windows in their own Space, without a toggle button or menubar, later + if (window->flags & SDL_WINDOW_RESIZABLE) { + // resizable windows are Spaces-friendly: they get the "go fullscreen" toggle button on their titlebar. + [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + } + } + + // Create a default view for this window + rect = [nswindow contentRectForFrameRect:[nswindow frame]]; + contentView = [[SDL3View alloc] initWithFrame:rect]; + [contentView setSDLWindow:window]; + nsview = contentView; + } + + if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) { + [nswindow setLevel:NSFloatingWindowLevel]; + } + + if (window->flags & SDL_WINDOW_TRANSPARENT) { + nswindow.opaque = NO; + nswindow.hasShadow = NO; + nswindow.backgroundColor = [NSColor clearColor]; + } + +// We still support OpenGL as long as Apple offers it, deprecated or not, so disable deprecation warnings about it. +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + /* Note: as of the macOS 10.15 SDK, this defaults to YES instead of NO when + * the NSHighResolutionCapable boolean is set in Info.plist. */ + BOOL highdpi = (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) ? YES : NO; + [nsview setWantsBestResolutionOpenGLSurface:highdpi]; +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef SDL_VIDEO_OPENGL_ES2 +#ifdef SDL_VIDEO_OPENGL_EGL + if ((window->flags & SDL_WINDOW_OPENGL) && + _this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_ES) { + [nsview setWantsLayer:TRUE]; + if ((window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY)) { + nsview.layer.contentsScale = nswindow.screen.backingScaleFactor; + } else { + nsview.layer.contentsScale = 1; + } + } +#endif // SDL_VIDEO_OPENGL_EGL +#endif // SDL_VIDEO_OPENGL_ES2 + [nswindow setContentView:nsview]; + + if (!SetupWindowData(_this, window, nswindow, nsview)) { + return false; + } + + if (!(window->flags & SDL_WINDOW_OPENGL)) { + return true; + } + + // The rest of this macro mess is for OpenGL or OpenGL ES windows +#ifdef SDL_VIDEO_OPENGL_ES2 + if (_this->gl_config.profile_mask == SDL_GL_CONTEXT_PROFILE_ES) { +#ifdef SDL_VIDEO_OPENGL_EGL + if (!Cocoa_GLES_SetupWindow(_this, window)) { + Cocoa_DestroyWindow(_this, window); + return false; + } + return true; +#else + return SDL_SetError("Could not create GLES window surface (EGL support not configured)"); +#endif // SDL_VIDEO_OPENGL_EGL + } +#endif // SDL_VIDEO_OPENGL_ES2 + return true; + } +} + +void Cocoa_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + const char *title = window->title ? window->title : ""; + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + NSString *string = [[NSString alloc] initWithUTF8String:title]; + [nswindow setTitle:string]; + } +} + +bool Cocoa_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon) +{ + @autoreleasepool { + NSImage *nsimage = Cocoa_CreateImage(icon); + + if (nsimage) { + [NSApp setApplicationIconImage:nsimage]; + + return true; + } + + return SDL_SetError("Unable to set the window's icon"); + } +} + +bool Cocoa_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = windata.nswindow; + NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]]; + BOOL fullscreen = (window->flags & SDL_WINDOW_FULLSCREEN) ? YES : NO; + int x, y; + + if ([windata.listener isInFullscreenSpaceTransition]) { + windata.pending_position = YES; + return true; + } + + if (!(window->flags & SDL_WINDOW_MAXIMIZED)) { + if (fullscreen) { + SDL_VideoDisplay *display = SDL_GetVideoDisplayForFullscreenWindow(window); + SDL_Rect r; + SDL_GetDisplayBounds(display->id, &r); + + rect.origin.x = r.x; + rect.origin.y = r.y; + } else { + SDL_RelativeToGlobalForWindow(window, window->pending.x, window->pending.y, &x, &y); + rect.origin.x = x; + rect.origin.y = y; + } + ConvertNSRect(&rect); + + // Position and constrain the popup + if (SDL_WINDOW_IS_POPUP(window)) { + NSRect screenRect = [ScreenForRect(&rect) frame]; + + if (rect.origin.x + rect.size.width > screenRect.origin.x + screenRect.size.width) { + rect.origin.x -= (rect.origin.x + rect.size.width) - (screenRect.origin.x + screenRect.size.width); + } + if (rect.origin.y + rect.size.height > screenRect.origin.y + screenRect.size.height) { + rect.origin.y -= (rect.origin.y + rect.size.height) - (screenRect.origin.y + screenRect.size.height); + } + rect.origin.x = SDL_max(rect.origin.x, screenRect.origin.x); + rect.origin.y = SDL_max(rect.origin.y, screenRect.origin.y); + } + + [nswindow setFrameOrigin:rect.origin]; + + ScheduleContextUpdates(windata); + } + } + return true; +} + +void Cocoa_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = windata.nswindow; + + if ([windata.listener isInFullscreenSpaceTransition]) { + windata.pending_size = YES; + return; + } + + if (!Cocoa_IsWindowZoomed(window)) { + int x, y; + NSRect rect = [nswindow contentRectForFrameRect:[nswindow frame]]; + + /* Cocoa will resize the window from the bottom-left rather than the + * top-left when -[nswindow setContentSize:] is used, so we must set the + * entire frame based on the new size, in order to preserve the position. + */ + SDL_RelativeToGlobalForWindow(window, window->floating.x, window->floating.y, &x, &y); + rect.origin.x = x; + rect.origin.y = y; + rect.size.width = window->pending.w; + rect.size.height = window->pending.h; + ConvertNSRect(&rect); + + [nswindow setFrame:[nswindow frameRectForContentRect:rect] display:YES]; + ScheduleContextUpdates(windata); + } else { + // Can't set the window size. + window->last_size_pending = false; + } + } +} + +void Cocoa_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal; + + NSSize minSize; + minSize.width = window->min_w; + minSize.height = window->min_h; + + [windata.nswindow setContentMinSize:minSize]; + } +} + +void Cocoa_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal; + + NSSize maxSize; + maxSize.width = window->max_w; + maxSize.height = window->max_h; + + [windata.nswindow setContentMaxSize:maxSize]; + } +} + +void Cocoa_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal; + + if (window->min_aspect > 0.0f && window->min_aspect == window->max_aspect) { + int numerator = 0, denominator = 1; + SDL_CalculateFraction(window->max_aspect, &numerator, &denominator); + [windata.nswindow setContentAspectRatio:NSMakeSize(numerator, denominator)]; + } else { + [windata.nswindow setContentAspectRatio:NSMakeSize(0, 0)]; + } + } +} + +void Cocoa_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h) +{ + @autoreleasepool { + SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal; + NSView *contentView = windata.sdlContentView; + NSRect viewport = [contentView bounds]; + + if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { + // This gives us the correct viewport for a Retina-enabled view. + viewport = [contentView convertRectToBacking:viewport]; + } + + *w = (int)viewport.size.width; + *h = (int)viewport.size.height; + } +} + +void Cocoa_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windowData = ((__bridge SDL_CocoaWindowData *)window->internal); + NSWindow *nswindow = windowData.nswindow; + bool bActivate = SDL_GetHintBoolean(SDL_HINT_WINDOW_ACTIVATE_WHEN_SHOWN, true); + + if (![nswindow isMiniaturized]) { + [windowData.listener pauseVisibleObservation]; + if (window->parent) { + NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow; + [nsparent addChildWindow:nswindow ordered:NSWindowAbove]; + + if (window->flags & SDL_WINDOW_MODAL) { + Cocoa_SetWindowModal(_this, window, true); + } + } + if (!SDL_WINDOW_IS_POPUP(window)) { + if (bActivate) { + [nswindow makeKeyAndOrderFront:nil]; + } else { + // Order this window below the key window if we're not activating it + if ([NSApp keyWindow]) { + [nswindow orderWindow:NSWindowBelow relativeTo:[[NSApp keyWindow] windowNumber]]; + } + } + } + } + [nswindow setIsVisible:YES]; + [windowData.listener resumeVisibleObservation]; + } +} + +void Cocoa_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + const BOOL waskey = [nswindow isKeyWindow]; + + /* orderOut has no effect on miniaturized windows, so close must be used to remove + * the window from the desktop and window list in this case. + * + * SDL holds a strong reference to the window (oneShot/releasedWhenClosed are 'NO'), + * and calling 'close' doesn't send a 'windowShouldClose' message, so it's safe to + * use for this purpose as nothing is implicitly released. + */ + if (![nswindow isMiniaturized]) { + [nswindow orderOut:nil]; + } else { + [nswindow close]; + } + + /* If this window is the source of a modal session, end it when + * hidden, or other windows will be prevented from closing. + */ + Cocoa_SetWindowModal(_this, window, false); + + // Transfer keyboard focus back to the parent when closing a popup menu + if (window->flags & SDL_WINDOW_POPUP_MENU) { + SDL_Window *new_focus = window->parent; + bool set_focus = window == SDL_GetKeyboardFocus(); + + // Find the highest level window, up to the toplevel parent, that isn't being hidden or destroyed. + while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) { + new_focus = new_focus->parent; + + // If some window in the chain currently had focus, set it to the new lowest-level window. + if (!set_focus) { + set_focus = new_focus == SDL_GetKeyboardFocus(); + } + } + + Cocoa_SetKeyboardFocus(new_focus, set_focus); + } else if (window->parent && waskey) { + /* Key status is not automatically set on the parent when a child is hidden. Check if the + * child window was key, and set the first visible parent to be key if so. + */ + SDL_Window *new_focus = window->parent; + + while (new_focus->parent != NULL && (new_focus->is_hiding || new_focus->is_destroying)) { + new_focus = new_focus->parent; + } + + if (new_focus) { + NSWindow *newkey = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + [newkey makeKeyAndOrderFront:nil]; + } + } + } +} + +void Cocoa_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windowData = ((__bridge SDL_CocoaWindowData *)window->internal); + NSWindow *nswindow = windowData.nswindow; + bool bActivate = SDL_GetHintBoolean(SDL_HINT_WINDOW_ACTIVATE_WHEN_RAISED, true); + + /* makeKeyAndOrderFront: has the side-effect of deminiaturizing and showing + a minimized or hidden window, so check for that before showing it. + */ + [windowData.listener pauseVisibleObservation]; + if (![nswindow isMiniaturized] && [nswindow isVisible]) { + if (window->parent) { + NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow; + [nsparent addChildWindow:nswindow ordered:NSWindowAbove]; + } + if (!SDL_WINDOW_IS_POPUP(window)) { + if (bActivate) { + [NSApp activateIgnoringOtherApps:YES]; + [nswindow makeKeyAndOrderFront:nil]; + } else { + [nswindow orderFront:nil]; + } + } else { + if (bActivate) { + [nswindow makeKeyWindow]; + } + } + } + [windowData.listener resumeVisibleObservation]; + } +} + +void Cocoa_MaximizeWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *windata = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = windata.nswindow; + + if ([windata.listener windowOperationIsPending:(PENDING_OPERATION_ENTER_FULLSCREEN | PENDING_OPERATION_LEAVE_FULLSCREEN)] || + [windata.listener isInFullscreenSpaceTransition]) { + Cocoa_SyncWindow(_this, window); + } + + if (!(window->flags & SDL_WINDOW_FULLSCREEN) && + ![windata.listener isInFullscreenSpaceTransition] && + ![windata.listener isInFullscreenSpace]) { + [nswindow zoom:nil]; + ScheduleContextUpdates(windata); + } else if (!windata.was_zoomed) { + [windata.listener addPendingWindowOperation:PENDING_OPERATION_ZOOM]; + } else { + [windata.listener clearPendingWindowOperation:PENDING_OPERATION_ZOOM]; + } + } +} + +void Cocoa_MinimizeWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = data.nswindow; + + [data.listener addPendingWindowOperation:PENDING_OPERATION_MINIMIZE]; + if ([data.listener isInFullscreenSpace] || (window->flags & SDL_WINDOW_FULLSCREEN)) { + [data.listener addPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN]; + SDL_UpdateFullscreenMode(window, false, true); + } else if ([data.listener isInFullscreenSpaceTransition]) { + [data.listener addPendingWindowOperation:PENDING_OPERATION_LEAVE_FULLSCREEN]; + } else { + [nswindow miniaturize:nil]; + } + } +} + +void Cocoa_RestoreWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = data.nswindow; + + if (([data.listener windowOperationIsPending:(PENDING_OPERATION_ENTER_FULLSCREEN | PENDING_OPERATION_LEAVE_FULLSCREEN)] && + ![data.nswindow isMiniaturized]) || [data.listener isInFullscreenSpaceTransition]) { + Cocoa_SyncWindow(_this, window); + } + + [data.listener clearPendingWindowOperation:(PENDING_OPERATION_MINIMIZE)]; + + if (!(window->flags & SDL_WINDOW_FULLSCREEN) && + ![data.listener isInFullscreenSpaceTransition] && + ![data.listener isInFullscreenSpace]) { + if ([nswindow isMiniaturized]) { + [nswindow deminiaturize:nil]; + } else if (Cocoa_IsWindowZoomed(window)) { + [nswindow zoom:nil]; + } + } else if (data.was_zoomed) { + [data.listener addPendingWindowOperation:PENDING_OPERATION_ZOOM]; + } else { + [data.listener clearPendingWindowOperation:PENDING_OPERATION_ZOOM]; + } + } +} + +void Cocoa_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + // If the window is in or transitioning to/from fullscreen, this will be set on leave. + if (!(window->flags & SDL_WINDOW_FULLSCREEN) && ![data.listener isInFullscreenSpaceTransition]) { + if (SetWindowStyle(window, GetWindowStyle(window))) { + if (bordered) { + Cocoa_SetWindowTitle(_this, window); // this got blanked out. + } + } + } else { + data.border_toggled = true; + } + Cocoa_UpdateClipCursor(window); + } +} + +void Cocoa_SetWindowResizable(SDL_VideoDevice *_this, SDL_Window *window, bool resizable) +{ + @autoreleasepool { + /* Don't set this if we're in or transitioning to/from a space! + * The window will get permanently stuck if resizable is false. + * -flibit + */ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + SDL3Cocoa_WindowListener *listener = data.listener; + NSWindow *nswindow = data.nswindow; + SDL_CocoaVideoData *videodata = data.videodata; + if (![listener isInFullscreenSpace] && ![listener isInFullscreenSpaceTransition]) { + SetWindowStyle(window, GetWindowStyle(window)); + } + if (videodata.allow_spaces) { + if (resizable) { + // resizable windows are Spaces-friendly: they get the "go fullscreen" toggle button on their titlebar. + [nswindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary]; + } else { + [nswindow setCollectionBehavior:NSWindowCollectionBehaviorManaged]; + } + } + } +} + +void Cocoa_SetWindowAlwaysOnTop(SDL_VideoDevice *_this, SDL_Window *window, bool on_top) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = data.nswindow; + + // If the window is in or transitioning to/from fullscreen, this will be set on leave. + if (!(window->flags & SDL_WINDOW_FULLSCREEN) && ![data.listener isInFullscreenSpaceTransition]) { + if (on_top) { + [nswindow setLevel:NSFloatingWindowLevel]; + } else { + [nswindow setLevel:kCGNormalWindowLevel]; + } + } + } +} + +SDL_FullscreenResult Cocoa_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = data.nswindow; + NSRect rect; + + // This is a synchronous operation, so always clear the pending flags. + [data.listener clearPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN | PENDING_OPERATION_LEAVE_FULLSCREEN]; + + // The view responder chain gets messed with during setStyleMask + if ([data.sdlContentView nextResponder] == data.listener) { + [data.sdlContentView setNextResponder:nil]; + } + + if (fullscreen) { + SDL_Rect bounds; + + if (!(window->flags & SDL_WINDOW_FULLSCREEN)) { + data.was_zoomed = !!(window->flags & SDL_WINDOW_MAXIMIZED); + } + + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_ENTER_FULLSCREEN, 0, 0); + Cocoa_GetDisplayBounds(_this, display, &bounds); + rect.origin.x = bounds.x; + rect.origin.y = bounds.y; + rect.size.width = bounds.w; + rect.size.height = bounds.h; + ConvertNSRect(&rect); + + /* Hack to fix origin on macOS 10.4 + This is no longer needed as of macOS 10.15, according to bug 4822. + */ + if (SDL_floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_14) { + NSRect screenRect = [[nswindow screen] frame]; + if (screenRect.size.height >= 1.0f) { + rect.origin.y += (screenRect.size.height - rect.size.height); + } + } + + [nswindow setStyleMask:NSWindowStyleMaskBorderless]; + } else { + NSRect frameRect; + + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0); + + rect.origin.x = data.was_zoomed ? window->windowed.x : window->floating.x; + rect.origin.y = data.was_zoomed ? window->windowed.y : window->floating.y; + rect.size.width = data.was_zoomed ? window->windowed.w : window->floating.w; + rect.size.height = data.was_zoomed ? window->windowed.h : window->floating.h; + + ConvertNSRect(&rect); + + /* The window is not meant to be fullscreen, but its flags might have a + * fullscreen bit set if it's scheduled to go fullscreen immediately + * after. Always using the windowed mode style here works around bugs in + * macOS 10.15 where the window doesn't properly restore the windowed + * mode decorations after exiting fullscreen-desktop, when the window + * was created as fullscreen-desktop. */ + [nswindow setStyleMask:GetWindowWindowedStyle(window)]; + + // Hack to restore window decorations on macOS 10.10 + frameRect = [nswindow frame]; + [nswindow setFrame:NSMakeRect(frameRect.origin.x, frameRect.origin.y, frameRect.size.width + 1, frameRect.size.height) display:NO]; + [nswindow setFrame:frameRect display:NO]; + } + + // The view responder chain gets messed with during setStyleMask + if ([data.sdlContentView nextResponder] != data.listener) { + [data.sdlContentView setNextResponder:data.listener]; + } + + [nswindow setContentSize:rect.size]; + [nswindow setFrameOrigin:rect.origin]; + + // When the window style changes the title is cleared + if (!fullscreen) { + Cocoa_SetWindowTitle(_this, window); + data.was_zoomed = NO; + if ([data.listener windowOperationIsPending:PENDING_OPERATION_ZOOM]) { + [data.listener clearPendingWindowOperation:PENDING_OPERATION_ZOOM]; + [nswindow zoom:nil]; + } + } + + if (SDL_ShouldAllowTopmost() && fullscreen) { + // OpenGL is rendering to the window, so make it visible! + [nswindow setLevel:kCGMainMenuWindowLevel + 1]; + } else if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) { + [nswindow setLevel:NSFloatingWindowLevel]; + } else { + [nswindow setLevel:kCGNormalWindowLevel]; + } + + if ([nswindow isVisible] || fullscreen) { + [data.listener pauseVisibleObservation]; + [nswindow makeKeyAndOrderFront:nil]; + [data.listener resumeVisibleObservation]; + } + + // Update the safe area insets + // The view never seems to reflect the safe area, so we'll use the screen instead + if (@available(macOS 12.0, *)) { + if (fullscreen) { + NSScreen *screen = [nswindow screen]; + + SDL_SetWindowSafeAreaInsets(data.window, + (int)SDL_ceilf(screen.safeAreaInsets.left), + (int)SDL_ceilf(screen.safeAreaInsets.right), + (int)SDL_ceilf(screen.safeAreaInsets.top), + (int)SDL_ceilf(screen.safeAreaInsets.bottom)); + } else { + SDL_SetWindowSafeAreaInsets(data.window, 0, 0, 0, 0); + } + } + + /* When coming out of fullscreen to minimize, this needs to happen after the window + * is made key again, or it won't minimize on 15.0 (Sequoia). + */ + if (!fullscreen && [data.listener windowOperationIsPending:PENDING_OPERATION_MINIMIZE]) { + Cocoa_WaitForMiniaturizable(window); + [data.listener addPendingWindowOperation:PENDING_OPERATION_ENTER_FULLSCREEN]; + [data.listener clearPendingWindowOperation:PENDING_OPERATION_MINIMIZE]; + [nswindow miniaturize:nil]; + } + + ScheduleContextUpdates(data); + Cocoa_SyncWindow(_this, window); + Cocoa_UpdateClipCursor(window); + } + + return SDL_FULLSCREEN_SUCCEEDED; +} + +void *Cocoa_GetWindowICCProfile(SDL_VideoDevice *_this, SDL_Window *window, size_t *size) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + NSWindow *nswindow = data.nswindow; + NSScreen *screen = [nswindow screen]; + NSData *iccProfileData = nil; + void *retIccProfileData = NULL; + + if (screen == nil) { + SDL_SetError("Could not get screen of window."); + return NULL; + } + + if ([screen colorSpace] == nil) { + SDL_SetError("Could not get colorspace information of screen."); + return NULL; + } + + iccProfileData = [[screen colorSpace] ICCProfileData]; + if (iccProfileData == nil) { + SDL_SetError("Could not get ICC profile data."); + return NULL; + } + + retIccProfileData = SDL_malloc([iccProfileData length]); + if (!retIccProfileData) { + return NULL; + } + + [iccProfileData getBytes:retIccProfileData length:[iccProfileData length]]; + *size = [iccProfileData length]; + return retIccProfileData; + } +} + +SDL_DisplayID Cocoa_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + NSScreen *screen; + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + // Not recognized via CHECK_WINDOW_MAGIC + if (data == nil) { + // Don't set the error here, it hides other errors and is ignored anyway + // return SDL_SetError("Window data not set"); + return 0; + } + + // NSWindow.screen may be nil when the window is off-screen. + screen = data.nswindow.screen; + + if (screen != nil) { + // https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc + CGDirectDisplayID displayid = [[screen.deviceDescription objectForKey:@"NSScreenNumber"] unsignedIntValue]; + SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid); + if (display) { + return display->id; + } + } + + // The higher level code will use other logic to find the display + return 0; + } +} + +bool Cocoa_SetWindowMouseRect(SDL_VideoDevice *_this, SDL_Window *window) +{ + Cocoa_UpdateClipCursor(window); + return true; +} + +bool Cocoa_SetWindowMouseGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + Cocoa_UpdateClipCursor(window); + + if (data && (window->flags & SDL_WINDOW_FULLSCREEN) != 0) { + if (SDL_ShouldAllowTopmost() && (window->flags & SDL_WINDOW_INPUT_FOCUS) && ![data.listener isInFullscreenSpace]) { + // OpenGL is rendering to the window, so make it visible! + // Doing this in 10.11 while in a Space breaks things (bug #3152) + [data.nswindow setLevel:kCGMainMenuWindowLevel + 1]; + } else if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) { + [data.nswindow setLevel:NSFloatingWindowLevel]; + } else { + [data.nswindow setLevel:kCGNormalWindowLevel]; + } + } + } + + return true; +} + +void Cocoa_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (SDL_CocoaWindowData *)CFBridgingRelease(window->internal); + + if (data) { +#ifdef SDL_VIDEO_OPENGL + + NSArray *contexts; + +#endif // SDL_VIDEO_OPENGL + SDL_Window *topmost = GetParentToplevelWindow(window); + SDL_CocoaWindowData *topmost_data = (__bridge SDL_CocoaWindowData *)topmost->internal; + + /* Reset the input focus of the root window if this window is still set as keyboard focus. + * SDL_DestroyWindow will have already taken care of reassigning focus if this is the SDL + * keyboard focus, this ensures that an inactive window with this window set as input focus + * does not try to reference it the next time it gains focus. + */ + if (topmost_data.keyboard_focus == window) { + SDL_Window *new_focus = window; + while (SDL_WINDOW_IS_POPUP(new_focus) && (new_focus->is_hiding || new_focus->is_destroying)) { + new_focus = new_focus->parent; + } + + topmost_data.keyboard_focus = new_focus; + } + + if ([data.listener isInFullscreenSpace]) { + [NSMenu setMenuBarVisible:YES]; + } + [data.listener close]; + data.listener = nil; + + if (!(window->flags & SDL_WINDOW_EXTERNAL)) { + // Release the content view to avoid further updateLayer callbacks + [data.nswindow setContentView:nil]; + [data.nswindow close]; + } + +#ifdef SDL_VIDEO_OPENGL + + contexts = [data.nscontexts copy]; + for (SDL3OpenGLContext *context in contexts) { + // Calling setWindow:NULL causes the context to remove itself from the context list. + [context setWindow:NULL]; + } + +#endif // SDL_VIDEO_OPENGL + } + window->internal = NULL; + } +} + +bool Cocoa_SetWindowFullscreenSpace(SDL_Window *window, bool state, bool blocking) +{ + @autoreleasepool { + bool succeeded = false; + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + if (state) { + data.fullscreen_space_requested = YES; + } + data.in_blocking_transition = blocking; + if ([data.listener setFullscreenSpace:(state ? YES : NO)]) { + if (blocking) { + const int maxattempts = 3; + int attempt = 0; + while (++attempt <= maxattempts) { + /* Wait for the transition to complete, so application changes + take effect properly (e.g. setting the window size, etc.) + */ + const int limit = 10000; + int count = 0; + while ([data.listener isInFullscreenSpaceTransition]) { + if (++count == limit) { + // Uh oh, transition isn't completing. Should we assert? + break; + } + SDL_Delay(1); + SDL_PumpEvents(); + } + if ([data.listener isInFullscreenSpace] == (state ? YES : NO)) { + break; + } + // Try again, the last attempt was interrupted by user gestures + if (![data.listener setFullscreenSpace:(state ? YES : NO)]) { + break; // ??? + } + } + } + + // Return TRUE to prevent non-space fullscreen logic from running + succeeded = true; + } + + data.in_blocking_transition = NO; + return succeeded; + } +} + +bool Cocoa_SetWindowHitTest(SDL_Window *window, bool enabled) +{ + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + [data.listener updateHitTest]; + return true; +} + +void Cocoa_AcceptDragAndDrop(SDL_Window *window, bool accept) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + if (accept) { + [data.nswindow registerForDraggedTypes:@[ (NSString *)kUTTypeFileURL, + (NSString *)kUTTypeUTF8PlainText ]]; + } else { + [data.nswindow unregisterDraggedTypes]; + } + } +} + +bool Cocoa_SetWindowParent(SDL_VideoDevice *_this, SDL_Window *window, SDL_Window *parent) +{ + @autoreleasepool { + SDL_CocoaWindowData *child_data = (__bridge SDL_CocoaWindowData *)window->internal; + + // Remove an existing parent. + if (child_data.nswindow.parentWindow) { + NSWindow *nsparent = ((__bridge SDL_CocoaWindowData *)window->parent->internal).nswindow; + [nsparent removeChildWindow:child_data.nswindow]; + } + + if (parent) { + SDL_CocoaWindowData *parent_data = (__bridge SDL_CocoaWindowData *)parent->internal; + [parent_data.nswindow addChildWindow:child_data.nswindow ordered:NSWindowAbove]; + } + } + + return true; +} + +bool Cocoa_SetWindowModal(SDL_VideoDevice *_this, SDL_Window *window, bool modal) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + if (data.modal_session) { + [NSApp endModalSession:data.modal_session]; + data.modal_session = nil; + } + + if (modal) { + data.modal_session = [NSApp beginModalSessionForWindow:data.nswindow]; + } + } + + return true; +} + +bool Cocoa_FlashWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_FlashOperation operation) +{ + @autoreleasepool { + // Note that this is app-wide and not window-specific! + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + if (data.flash_request) { + [NSApp cancelUserAttentionRequest:data.flash_request]; + data.flash_request = 0; + } + + switch (operation) { + case SDL_FLASH_CANCEL: + // Canceled above + break; + case SDL_FLASH_BRIEFLY: + data.flash_request = [NSApp requestUserAttention:NSInformationalRequest]; + break; + case SDL_FLASH_UNTIL_FOCUSED: + data.flash_request = [NSApp requestUserAttention:NSCriticalRequest]; + break; + default: + return SDL_Unsupported(); + } + return true; + } +} + +bool Cocoa_SetWindowFocusable(SDL_VideoDevice *_this, SDL_Window *window, bool focusable) +{ + return true; // just succeed, the real work is done elsewhere. +} + +bool Cocoa_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity) +{ + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + [data.nswindow setAlphaValue:opacity]; + return true; + } +} + +bool Cocoa_SyncWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + bool result = true; + + @autoreleasepool { + SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; + + do { + SDL_PumpEvents(); + } while ([data.listener hasPendingWindowOperation]); + } + + return result; +} + +#endif // SDL_VIDEO_DRIVER_COCOA -- cgit v1.2.3