We are trying to use SDL3 together with JUCE, a framework for audio plugins including a complete UI system. This is not the typical way to use SDL, and we got many event-related issues in Mac. We find these modifications are useful to cooperate SDL3 with another UI system that already has event handling, or in a non-monolithic process.
I’m not familiar with Apple Cocoa. Though these codes are generated by AI agent, they look reasonable, really fix our issue, and make no harmful artifacts in our preliminary testing plugin. So I just post them here as a suggestion or hint to other people who may use SDL3 in untypical way like ours.
No similar issue were found in Windows and Linux X11. Maybe Apple Cocoa is really different.
Firstly, make SDL3 only consume events that are targeting to SDL3-owned window. This avoids SDL3 to disturb host DAW’s mouse interactions, such as resizing plugin window.
// in SDL_cocoaevents.m
// add a function that checks if a event belongs to SDL-owned window
static bool Cocoa_EventBelongsToSDLCreatedWindow(NSEvent *event)
{
NSWindow *eventWindow = [event window];
if (!eventWindow) {
const NSInteger windowNumber = [event windowNumber];
if (windowNumber != 0) {
eventWindow = [NSApp windowWithWindowNumber:windowNumber];
}
}
if (!eventWindow) {
return false;
}
SDL_Window *sdlwindow = FindSDLWindowForNSWindow(eventWindow);
if (!sdlwindow) {
return false;
}
return (sdlwindow->flags & SDL_WINDOW_EXTERNAL) == 0;
}
// inside the infinite for loop of Cocoa_PumpEventsUntilDate, use the function above to skip events that do not belong to SDL3-handled window
int Cocoa_PumpEventsUntilDate(SDL_VideoDevice *_this, NSDate *expiration, bool accumulate)
{
// ... Run any existing modal sessions ...
for (;;) {
if (!s_bShouldHandleEventsInSDLApplication) {
NSEvent *peeked = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:NO];
if (peeked == nil) {
return 0;
}
if (!Cocoa_EventBelongsToSDLCreatedWindow(peeked)) {
return 0;
}
}
// ... lots of things todo ...
}
return 1;
}
Second, these modifications avoids SDL3 losses keyboard typings:
// in SDL_cocoaevents.m
// add some utility functions
static bool SDL__ShouldBridgeExternalKeyEvents(void)
{
return SDL_GetHintBoolean("SDL__BRIDGE_EXTERNAL_KEY_EVENTS", true);
}
static bool SDL__IsKeyboardNSEvent(NSEvent *event)
{
const NSEventType type = [event type];
return type == NSEventTypeKeyDown || type == NSEventTypeKeyUp || type == NSEventTypeFlagsChanged;
}
static id s_ExternalKeyMonitor = nil;
static void SDL__InstallExternalKeyMonitorIfNeeded(void)
{
if (s_bShouldHandleEventsInSDLApplication || s_ExternalKeyMonitor != nil || !SDL__ShouldBridgeExternalKeyEvents()) {
return;
}
s_ExternalKeyMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskKeyUp | NSEventMaskFlagsChanged)
handler:^NSEvent *(NSEvent *event) {
SDL_VideoDevice *_this = SDL_GetVideoDevice();
if (_this && Cocoa_EventBelongsToSDLCreatedWindow(event)) {
if (SDL__ShouldTraceKeyPipeline()) {
SDL_Log("[KEY][LOCAL_MONITOR_HIT] type=%d keyCode=%hu", (int)[event type], [event keyCode]);
}
Cocoa_DispatchEvent(event);
}
return event;
}];
if (SDL__ShouldTraceKeyPipeline()) {
SDL_Log("[KEY][LOCAL_MONITOR_INSTALLED] enabled=1");
}
}
// at last of Cocoa_RegisterApp, install the hook
void Cocoa_RegisterApp(void)
{
@autoreleasepool {
// ......
SDL__InstallExternalKeyMonitorIfNeeded();
}
}
// a lot of modifications here, I just paste the entire function
// in brief, it pumps all events, and repumps non-SDL-owned events again
int Cocoa_PumpEventsUntilDate(SDL_VideoDevice *_this, NSDate *expiration, bool accumulate)
{
const bool trace_keypipe = SDL__ShouldTraceKeyPipeline();
const bool bridge_external_key_events = SDL__ShouldBridgeExternalKeyEvents();
// 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 (;;) {
if (!s_bShouldHandleEventsInSDLApplication) {
NSEvent *peeked = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:NO];
if (peeked == nil) {
return 0;
}
const bool belongs_to_sdl_window = Cocoa_EventBelongsToSDLCreatedWindow(peeked);
if (!belongs_to_sdl_window) {
/*
* In host-driven apps (e.g. JUCE creates NSApp), the queue can contain
* non-SDL events before SDL-owned events. If we return here, SDL may skip
* an entire pump cycle and miss keyboard/text events that arrive right after.
*
* Consume one foreign event and forward it to NSApp, then continue scanning.
*/
NSEvent *foreignEvent = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:YES];
if (foreignEvent == nil) {
return 0;
}
[NSApp sendEvent:foreignEvent];
if (!accumulate) {
break;
}
continue;
}
}
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:expiration inMode:NSDefaultRunLoopMode dequeue:YES];
if (event == nil) {
return 0;
}
if (!s_bShouldHandleEventsInSDLApplication) {
if (bridge_external_key_events && SDL__IsKeyboardNSEvent(event)) {
} else {
Cocoa_DispatchEvent(event);
}
}
// Pass events down to SDL3Application to be handled in sendEvent:
[NSApp sendEvent:event];
if (!accumulate) {
break;
}
}
return 1;
}
Hopefully these stuffs are helpful or make inspirations. Thank you guys!