Event Loop with Cocoa on MacOS

References

We already had a basic event loop just to make the window show up, but now we'll handle all the events you're likely to need for your game/app. The setup code is mostly the same; the delegates and event loop have extra code this time.

Build with:

clang main.m -framework Cocoa

Most events are self-explanatory, but I'll explore a few.

case NSEventTypeScrollWheel: { if ([e hasPreciseScrollingDeltas]) { const NSTimeInterval time = [e timestamp]; const float dy = [e scrollingDeltaY]; printf ("Precise scroll %f @ %f\n", dy, time); } else { const float dy = [e deltaY]; printf ("Simple scroll %f\n", dy); } } break;

Scrolling on MacOS for games is a disaster. Mac uses "precise scrolling," which is great if you want to smoothly change a continuous value with the scroll wheel/ball (like moving around a map, say), but horrible for switching weapons with the scroll wheel in a shooter. You may just need to ask the user what kind of scrolling device they're using. Some old mice *might* send simple scrolls. Every modern, non-Apple mouse I've tried has sent a bunch of precise scroll events for each wheel notch. I don't own an Apple mouse, but apparently they'll send loads of tiny scrolls. The second case (non-Apple modern mice) can be handled fairly well by filtering out any events with a scrollingDeltaY of 0, then recording the last timestamp and ignoring subsequent events with the same timestamp. As for using Apple mice in the discrete way games tend to prefer: good luck!

case NSEventTypeFlagsChanged: { typedef union { struct { uint8_t alpha_shift:1; uint8_t shift:1; uint8_t control:1; uint8_t alternate:1; uint8_t command:1; uint8_t numeric_pad:1; uint8_t help:1; uint8_t function:1; }; uint8_t mask; } osx_event_modifiers_t; static osx_event_modifiers_t mods_prev = {}; osx_event_modifiers_t mods = {.mask = ([e modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask) >> 16}; if (mods.alpha_shift ^ mods_prev.alpha_shift) printf ("Key %s Alpha Shift\n", mods.alpha_shift ? "pressed" : "released"); if (mods.shift ^ mods_prev.shift) printf ("Key %s Shift\n", mods.shift ? "pressed" : "released"); if (mods.control ^ mods_prev.control) printf ("Key %s Ctrl\n", mods.control ? "pressed" : "released"); if (mods.alternate ^ mods_prev.alternate) printf ("Key %s Alt\n", mods.alternate ? "pressed" : "released"); if (mods.command ^ mods_prev.command) printf ("Key %s Command\n", mods.alpha_shift ? "pressed" : "released"); if (mods.numeric_pad ^ mods_prev.numeric_pad) printf ("Key %s NumLock\n", mods.numeric_pad ? "pressed" : "released"); if (mods.help ^ mods_prev.help) printf ("Key %s Help\n", mods.help ? "pressed" : "released"); if (mods.function ^ mods_prev.function) printf ("Key %s Function\n", mods.function ? "pressed" : "released"); mods_prev = mods; } break;

Cocoa sets aside various keys like Alt, CTRL, etc. and sends them as a group of flags instead of normal key events. Here I record the last collective flag state and check for any changes each time this event comes in. Note that some of these keys represent different things on different types of keyboards.

case NSEventTypeApplicationDefined: { switch ([e subtype]) { case OSXUserEvent_WindowClose: { printf ("Window close\n"); quit = true; } break; case OSXUserEvent_WindowResize: printf ("Resize %d, %d\n", (int)[e data1], (int)[e data2]); break; case OSXUserEvent_LostFocus: puts ("Focus Out"); break; case OSXUserEvent_EnterFullscreen: puts ("Fullscreen"); break; case OSXUserEvent_ExitFullscreen: puts ("Exit fullscreen"); break; default: puts ("Unknown user event?"); break; } } break;

Ah, the user events! Finally, something that just works exactly as you'd expect.

enum { OSXUserEvent_WindowClose, OSXUserEvent_WindowResize, OSXUserEvent_LostFocus, OSXUserEvent_EnterFullscreen, OSXUserEvent_ExitFullscreen, };

These are user event values. You can set them to anything - they're entirely created and used by your own code. I like to just put them in an enum to give them all a unique value.

NSView *content_view = [window contentView]; NSTrackingArea *tracking_area = [[NSTrackingArea alloc] initWithRect:content_view.bounds options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect owner:[window delegate] userInfo:nil]; [content_view addTrackingArea:tracking_area];

Mouse events in Cocoa are tricky. You can receive events with NSEventTypeMouseMoved, but I'd like to ignore events outside the content area of the window. For this purpose, and showing/hiding the cursor in a later tutorial, we create an NSTrackingArea. The options tell Cocoa what types of events we want to receive, and we add the tracking area to the contentView of our window. This will automatically updated whenever the contentView is resized.

- (void)mouseEntered:(NSEvent *)event { printf("Mouse entered content view tracking area\n"); } - (void)mouseExited:(NSEvent *)event { printf("Mouse exited content view tracking area\n"); } - (void)mouseMoved:(NSEvent *)event { NSPoint p = [[window contentView] convertPointToBacking:[event locationInWindow]]; printf("Mouse moved inside content view tracking area: %.1f %.1f\n", p.x, p.y); }

Since we set the NSTrackingMouseEnteredAndExited and NSTrackingMouseMoved options, we must implement the associated functions to be called. Because of MacOS's HiDPI retina stuff, we can't just take the [event locationInWindow] point and have it relate directly to rendering coordinates, so we convert it to the coordinate space of the content view backing. Note that the (0,0) point is the bottom-left of your content view. If you wanted to receive mouse movement events in your event loop, you might create a user event inside the mouseMoved function. Speaking of which...

-(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender { NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:(NSPoint){0,0} modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil subtype:OSXUserEvent_WindowClose data1:0 data2:0]; [NSApp postEvent:event atStart:true]; return NSTerminateCancel; }

We create an event with [NSEvent otherEventWithType:NSEventTypeApplicationDefined ...]. subtype is where we use our custom OSXUserEvent enum. In [NSApp postEvent ...] you have the choice of posting it to the start of the event queue, so that it's the next event to be processed. I only use this for quit events, since there's no point processing remaining events.

-(void)windowDidResignKey:(NSNotification*)notification { NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:(NSPoint){0,0} modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil subtype:OSXUserEvent_LostFocus data1:0 data2:0]; [NSApp postEvent:event atStart:false]; }

windowDidResignKey means your window is no longer the "key window" - Apple's term for the window with focus.

-(void)windowDidResize:(NSNotification *)notification { if (live_resizing) return; NSSize size = WINDOW_CONTENT_SIZE; NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:(NSPoint){0,0} modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil subtype:OSXUserEvent_WindowResize data1:size.width data2:size.height]; [NSApp postEvent:event atStart:false]; }

Here you see how to use the data1 and data2 fields to pass additional information along with an event. Note the if (live_resizing) check - you don't want to post thousands of resize events during a live resize, as that can cause live resizing to slow down tremendously. Since we aren't doing much on resize it may not show an effect, but if you do anything significant on resize, this can be very slow. Try commenting out this check and see how many resize events are posted!

-(void)windowWillStartLiveResize:(NSNotification *)notification { live_resizing = true; } -(void)windowDidEndLiveResize:(NSNotification *)notification { live_resizing = false; NSSize size = WINDOW_CONTENT_SIZE; NSEvent *event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:(NSPoint){0,0} modifierFlags:0 timestamp:[[NSProcessInfo processInfo] systemUptime] windowNumber:[window windowNumber] context:nil subtype:OSXUserEvent_WindowResize data1:size.width data2:size.height]; [NSApp postEvent:event atStart:false]; }

Here's live resizing. On start, I just set the variable to disable resize events. On end, I post a resize event and clear the live_resizing variable.