chore(build): use SDL3

This commit is contained in:
phaneron 2025-04-12 04:38:19 -04:00
parent 9d04a35d87
commit b3c0734a9e
3286 changed files with 866354 additions and 554996 deletions

View file

@ -0,0 +1,34 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,262 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
@interface Cocoa_PasteboardDataProvider : NSObject<NSPasteboardItemDataProvider>
{
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<NSPasteboardItem *> *items = [[NSPasteboard generalPasteboard] pasteboardItems];
NSUInteger nitems = [items count];
if (nitems > 0) {
for (NSPasteboardItem *item in items) {
NSArray<NSString *> *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<NSString *> *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

View file

@ -0,0 +1,33 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,680 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <NSApplicationDelegate>
{
@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

View file

@ -0,0 +1,36 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,604 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <Carbon/Carbon.h>
#if 0
#define DEBUG_IME NSLog
#else
#define DEBUG_IME(...)
#endif
@interface SDL3TranslatorResponder : NSView <NSTextInputClient>
{
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 <https://discourse.libsdl.org/> or to Christian Walther <cwalther@gmx.ch>. 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

View file

@ -0,0 +1,27 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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

View file

@ -0,0 +1,145 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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

View file

@ -0,0 +1,66 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <Cocoa/Cocoa.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
@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_

View file

@ -0,0 +1,182 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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)

View file

@ -0,0 +1,45 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,718 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <IOKit/graphics/IOGraphicsLib.h>
// We need this for CVDisplayLinkGetNominalOutputVideoRefreshPeriod
#include <CoreVideo/CVBase.h>
#include <CoreVideo/CVDisplayLink.h>
#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)
{
@autoreleasepool {
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

View file

@ -0,0 +1,51 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,591 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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

View file

@ -0,0 +1,88 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <Cocoa/Cocoa.h>
#import <QuartzCore/CVDisplayLink.h>
// 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_

View file

@ -0,0 +1,559 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <OpenGL/CGLTypes.h>
#include <OpenGL/OpenGL.h>
#include <OpenGL/CGLRenderers.h>
#include <SDL3/SDL_opengl.h>
#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

View file

@ -0,0 +1,48 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,156 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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

View file

@ -0,0 +1,32 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,178 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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

View file

@ -0,0 +1,28 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,54 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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

View file

@ -0,0 +1,71 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <SDL3/SDL_opengl.h>
#include <ApplicationServices/ApplicationServices.h>
#include <IOKit/pwr_mgt/IOPMLib.h>
#include <Cocoa/Cocoa.h>
#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_

View file

@ -0,0 +1,337 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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

View file

@ -0,0 +1,52 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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_

View file

@ -0,0 +1,304 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <dlfcn.h>
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

View file

@ -0,0 +1,199 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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 <Cocoa/Cocoa.h>
#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 <NSWindowDelegate>
{
/* 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_

File diff suppressed because it is too large Load diff