summary refs log tree commit diff
path: root/src/Window_cocoa.m
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Window_cocoa.m
initial commit
Diffstat (limited to 'src/Window_cocoa.m')
-rw-r--r--src/Window_cocoa.m961
1 files changed, 961 insertions, 0 deletions
diff --git a/src/Window_cocoa.m b/src/Window_cocoa.m
new file mode 100644
index 0000000..5891b23
--- /dev/null
+++ b/src/Window_cocoa.m
@@ -0,0 +1,961 @@
+// Silence deprecation warnings on modern macOS
+#define GL_SILENCE_DEPRECATION
+
+#include "Core.h"
+#if CC_WIN_BACKEND == CC_WIN_BACKEND_COCOA
+#include "_WindowBase.h"
+#include "ExtMath.h"
+#include "Funcs.h"
+#include "Bitmap.h"
+#include "String.h"
+#include "Options.h"
+#import  <Foundation/Foundation.h>
+#import  <AppKit/AppKit.h>
+#include <ApplicationServices/ApplicationServices.h>
+
+static int windowX, windowY;
+static NSApplication* appHandle;
+static NSWindow* winHandle;
+static NSView* viewHandle;
+static cc_bool canCheckOcclusion;
+static cc_bool legacy_fullscreen;
+static cc_bool scroll_debugging;
+
+#if defined MAC_OS_X_VERSION_10_12 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_12
+	#define WIN_MASK (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable)
+	#define ANY_EVENT_MASK NSEventMaskAny
+	#define DIALOG_OK      NSModalResponseOK
+	
+	#define PASTEBOARD_STRING_TYPE NSPasteboardTypeString
+#else
+	#define WIN_MASK (NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask)
+	#define ANY_EVENT_MASK NSAnyEventMask
+	#define DIALOG_OK      NSOKButton
+	
+	#define PASTEBOARD_STRING_TYPE NSStringPboardType
+#endif
+
+extern size_t CGDisplayBitsPerPixel(CGDirectDisplayID display);
+// TODO: Try replacing with NSBitsPerPixelFromDepth([NSScreen mainScreen].depth) instead
+
+// Sourced from https://www.meandmark.com/keycodes.html
+static const cc_uint8 key_map[8 * 16] = {
+/* 0x00 */ 'A', 'S', 'D', 'F', 'H', 'G', 'Z', 'X',
+/* 0x08 */ 'C', 'V',   0, 'B', 'Q', 'W', 'E', 'R',
+/* 0x10 */ 'Y', 'T', '1', '2', '3', '4', '6', '5',
+/* 0x18 */ CCKEY_EQUALS, '9', '7', CCKEY_MINUS, '8', '0', CCKEY_RBRACKET, 'O',
+/* 0x20 */ 'U', CCKEY_LBRACKET, 'I', 'P', CCKEY_ENTER, 'L', 'J', CCKEY_QUOTE,
+/* 0x28 */ 'K', CCKEY_SEMICOLON, CCKEY_BACKSLASH, CCKEY_COMMA, CCKEY_SLASH, 'N', 'M', CCKEY_PERIOD,
+/* 0x30 */ CCKEY_TAB, CCKEY_SPACE, CCKEY_TILDE, CCKEY_BACKSPACE, 0, CCKEY_ESCAPE, 0, 0,
+/* 0x38 */ 0, CCKEY_CAPSLOCK, 0, 0, 0, 0, 0, 0,
+/* 0x40 */ 0, CCKEY_KP_DECIMAL, 0, CCKEY_KP_MULTIPLY, 0, CCKEY_KP_PLUS, 0, CCKEY_NUMLOCK,
+/* 0x48 */ CCKEY_VOLUME_UP, CCKEY_VOLUME_DOWN, CCKEY_VOLUME_MUTE, CCKEY_KP_DIVIDE, CCKEY_KP_ENTER, 0, CCKEY_KP_MINUS, 0,
+/* 0x50 */ 0, CCKEY_KP_ENTER, CCKEY_KP0, CCKEY_KP1, CCKEY_KP2, CCKEY_KP3, CCKEY_KP4, CCKEY_KP5,
+/* 0x58 */ CCKEY_KP6, CCKEY_KP7, 0, CCKEY_KP8, CCKEY_KP9, 'N', 'M', CCKEY_PERIOD,
+/* 0x60 */ CCKEY_F5, CCKEY_F6, CCKEY_F7, CCKEY_F3, CCKEY_F8, CCKEY_F9, 0, CCKEY_F11,
+/* 0x68 */ 0, CCKEY_F13, 0, CCKEY_F14, 0, CCKEY_F10, 0, CCKEY_F12,
+/* 0x70 */ 'U', CCKEY_F15, CCKEY_INSERT, CCKEY_HOME, CCKEY_PAGEUP, CCKEY_DELETE, CCKEY_F4, CCKEY_END,
+/* 0x78 */ CCKEY_F2, CCKEY_PAGEDOWN, CCKEY_F1, CCKEY_LEFT, CCKEY_RIGHT, CCKEY_DOWN, CCKEY_UP, 0,
+};
+static int MapNativeKey(UInt32 key) { return key < Array_Elems(key_map) ? key_map[key] : 0; }
+// TODO: Check these..
+//   case 0x37: return CCKEY_LWIN;
+//   case 0x38: return CCKEY_LSHIFT;
+//   case 0x3A: return CCKEY_LALT;
+//   case 0x3B: return Key_ControlLeft;
+
+// TODO: Verify these differences from OpenTK
+//Backspace = 51,  (0x33, CCKEY_DELETE according to that link)
+//Return = 52,     (0x34, ??? according to that link)
+//Menu = 110,      (0x6E, ??? according to that link)
+
+
+/*########################################################################################################################*
+ *---------------------------------------------------------Cursor---------------------------------------------------------*
+ *#########################################################################################################################*/
+static cc_bool warping;
+static int warpDX, warpDY;
+
+static cc_bool GetMouseCoords(int* x, int* y) {
+	NSPoint loc = [NSEvent mouseLocation];
+	*x = (int)loc.x                        - windowX;
+	*y = (DisplayInfo.Height - (int)loc.y) - windowY;
+	// TODO: this seems to be off by 1
+	return *x >= 0 && *y >= 0 && *x < Window_Main.Width && *y < Window_Main.Height;
+}
+
+static void ProcessRawMouseMovement(NSEvent* ev) {
+	float dx = [ev deltaX];
+	float dy = [ev deltaY];
+
+	if (warping) { dx -= warpDX; dy -= warpDY; }
+	Event_RaiseRawMove(&PointerEvents.RawMoved, dx, dy);
+}
+
+
+void Cursor_SetPosition(int x, int y) {
+	int curX, curY;
+	GetMouseCoords(&curX, &curY);
+
+	CGPoint point;
+	point.x = x + windowX;
+	point.y = y + windowY;
+	CGDisplayMoveCursorToPoint(CGMainDisplayID(), point);
+
+	// Next mouse movement event will include the delta from
+	//  this warp - so need to adjust processing to remove the delta
+	warping = true;
+	warpDX  = x - curX;
+	warpDY  = y - curY;
+}
+
+static void Cursor_DoSetVisible(cc_bool visible) {
+	if (visible) {
+		CGDisplayShowCursor(CGMainDisplayID());
+	} else {
+		CGDisplayHideCursor(CGMainDisplayID());
+	}
+}
+
+void Window_EnableRawMouse(void) {
+	CGAssociateMouseAndMouseCursorPosition(NO);
+	DefaultEnableRawMouse();
+}
+
+void Window_UpdateRawMouse(void) { }
+void Cursor_GetRawPos(int* x, int* y) { *x = 0; *y = 0; }
+
+void Window_DisableRawMouse(void) {
+	DefaultDisableRawMouse();
+	CGAssociateMouseAndMouseCursorPosition(YES);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------------General---------------------------------------------------------*
+*#########################################################################################################################*/
+void Clipboard_GetText(cc_string* value) {
+	NSPasteboard* pasteboard;
+	const char* src;
+	NSString* str;
+	int len;
+
+	pasteboard = [NSPasteboard generalPasteboard];
+	str        = [pasteboard stringForType:PASTEBOARD_STRING_TYPE];
+
+	if (!str) return;
+	src = [str UTF8String];
+	len = String_Length(src);
+	String_AppendUtf8(value, src, len);
+}
+
+void Clipboard_SetText(const cc_string* value) {
+	NSPasteboard* pasteboard;
+	char raw[NATIVE_STR_LEN];
+	NSString* str;
+
+	String_EncodeUtf8(raw, value);
+	str        = [NSString stringWithUTF8String:raw];
+	pasteboard = [NSPasteboard generalPasteboard];
+
+	[pasteboard declareTypes:[NSArray arrayWithObject:PASTEBOARD_STRING_TYPE] owner:nil];
+	[pasteboard setString:str forType:PASTEBOARD_STRING_TYPE];
+}
+
+
+static void LogUnhandled(NSString* str) {
+	if (!str) return;
+	const char* src = [str UTF8String];
+	if (!src) return;
+	
+	cc_string msg = String_FromReadonly(src);
+	Platform_Log(msg.buffer, msg.length);
+	Logger_Log(&msg);
+}
+
+// TODO: Should really be handled elsewhere, in Logger or ErrorHandler
+static void LogUnhandledNSErrors(NSException* ex) {
+	// last chance to log exception details before process dies
+	LogUnhandled(@"About to die from unhandled NSException..");
+	LogUnhandled([ex name]);
+	LogUnhandled([ex reason]);
+}
+
+void Window_PreInit(void) {
+	NSSetUncaughtExceptionHandler(LogUnhandledNSErrors);
+}
+
+static NSAutoreleasePool* pool;
+void Window_Init(void) {
+	Input.Sources = INPUT_SOURCE_NORMAL;
+
+	// https://www.cocoawithlove.com/2009/01/demystifying-nsapplication-by.html
+	pool = [[NSAutoreleasePool alloc] init];
+	appHandle = [NSApplication sharedApplication];
+	[appHandle activateIgnoringOtherApps:YES];
+
+	CGDirectDisplayID display = CGMainDisplayID();
+	CGRect bounds = CGDisplayBounds(display);
+
+	DisplayInfo.x      = (int)bounds.origin.x;
+	DisplayInfo.y      = (int)bounds.origin.y;
+	DisplayInfo.Width  = (int)bounds.size.width;
+	DisplayInfo.Height = (int)bounds.size.height;
+	DisplayInfo.Depth  = CGDisplayBitsPerPixel(display);
+	DisplayInfo.ScaleX = 1;
+	DisplayInfo.ScaleY = 1;
+
+	// NSApplication sometimes replaces the uncaught exception handler, so set it again
+	NSSetUncaughtExceptionHandler(LogUnhandledNSErrors);
+}
+
+void Window_Free(void) { }
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------------Window--------------------------------------------------------*
+*#########################################################################################################################*/
+#if !defined MAC_OS_X_VERSION_10_4
+// Doesn't exist in < 10.4 SDK. No issue since < 10.4 is only Big Endian PowerPC anyways
+#define kCGBitmapByteOrder32Host 0
+#endif
+
+static void RefreshWindowBounds(void) {
+	if (legacy_fullscreen) {
+		CGRect rect = CGDisplayBounds(CGMainDisplayID());
+		windowX = (int)rect.origin.x; // usually 0
+		windowY = (int)rect.origin.y; // usually 0
+		// TODO is it correct to use display bounds and not just 0?
+		
+		Window_Main.Width  = (int)rect.size.width;
+		Window_Main.Height = (int)rect.size.height;
+		return;
+	}
+
+	NSRect win  = [winHandle frame];
+	NSRect view = [viewHandle frame];
+	int viewY;
+
+	// For cocoa, the 0,0 origin is the bottom left corner of windows/views/screen.
+	// To get window's real Y screen position, first need to find Y of top. (win.y + win.height)
+	// Then just subtract from screen height to make relative to top instead of bottom of the screen.
+	// Of course this is only half the story, since we're really after Y position of the content.
+	// To work out top Y of view relative to window, it's just win.height - (view.y + view.height)
+	viewY   = (int)win.size.height - ((int)view.origin.y + (int)view.size.height);
+	windowX = (int)win.origin.x    + (int)view.origin.x;
+	windowY = DisplayInfo.Height   - ((int)win.origin.y  + (int)win.size.height) + viewY;
+
+	Window_Main.Width  = (int)view.size.width;
+	Window_Main.Height = (int)view.size.height;
+}
+
+@interface CCWindow : NSWindow { }
+@end
+@implementation CCWindow
+// If this isn't overriden, an annoying beep sound plays anytime a key is pressed
+- (void)keyDown:(NSEvent *)event { }
+@end
+
+@interface CCWindowDelegate : NSObject { }
+@end
+@implementation CCWindowDelegate
+- (void)windowDidResize:(NSNotification *)notification {
+	RefreshWindowBounds();
+	Event_RaiseVoid(&WindowEvents.Resized);
+}
+
+- (void)windowDidMove:(NSNotification *)notification {
+	RefreshWindowBounds();
+#if (CC_GFX_BACKEND & CC_GFX_BACKEND_GL_MASK)
+	GLContext_Update();
+#endif
+}
+
+- (void)windowDidBecomeKey:(NSNotification *)notification {
+	Window_Main.Focused = true;
+	Event_RaiseVoid(&WindowEvents.FocusChanged);
+}
+
+- (void)windowDidResignKey:(NSNotification *)notification {
+	Window_Main.Focused = false;
+	Event_RaiseVoid(&WindowEvents.FocusChanged);
+}
+
+- (void)windowDidMiniaturize:(NSNotification *)notification {
+	Event_RaiseVoid(&WindowEvents.StateChanged);
+}
+
+- (void)windowDidDeminiaturize:(NSNotification *)notification {
+	Event_RaiseVoid(&WindowEvents.StateChanged);
+}
+
+- (void)windowWillClose:(NSNotification *)notification {
+	Window_Main.Exists = false;
+	Event_RaiseVoid(&WindowEvents.Closing);
+}
+@end
+
+
+static void DoDrawFramebuffer(NSRect dirty);
+@interface CCView : NSView { }
+@end
+@implementation CCView
+
+- (void)drawRect:(NSRect)dirty { DoDrawFramebuffer(dirty); }
+
+- (void)viewDidEndLiveResize {
+	// When the user users left mouse to drag reisze window, this enters 'live resize' mode
+	//   Although the game receives a left mouse down event, it does NOT receive a left mouse up
+	//   This causes the game to get stuck with left mouse down after user finishes resizing
+	// So work arond that by always releasing left mouse when a live resize is finished
+	Input_SetReleased(CCMOUSE_L);
+}
+@end
+
+static void MakeContentView(void) {
+	NSRect rect;
+	NSView* view;
+
+	view = [winHandle contentView];
+	rect = [view frame];
+
+	viewHandle = [CCView alloc];
+	[viewHandle initWithFrame:rect];
+	[winHandle setContentView:viewHandle];
+}
+
+#ifdef CC_BUILD_ICON
+// See misc/macOS/mac_icon_gen.cs for how to generate this file
+#include "../misc/macOS/CCIcon_mac.h"
+
+static void ApplyIcon(void) {
+	NSImage* img;
+	const unsigned int* pixels = CCIcon_Data;
+	unsigned char** planes     = (unsigned char**)&pixels;
+    
+	NSBitmapImageRep* rep = [NSBitmapImageRep alloc];
+	rep = [rep initWithBitmapDataPlanes:planes
+						pixelsWide:CCIcon_Width pixelsHigh:CCIcon_Height
+						bitsPerSample:8 samplesPerPixel:4
+						hasAlpha:YES isPlanar:NO
+						colorSpaceName:NSDeviceRGBColorSpace
+						bytesPerRow:CCIcon_Width * 4
+						bitsPerPixel:32];
+
+	img = [NSImage alloc];
+	img = [img initWithSize:NSMakeSize(CCIcon_Width, CCIcon_Height)];
+	[img addRepresentation:rep];
+	[appHandle setApplicationIconImage:img];
+	//[img release];
+}
+#else
+static void ApplyIcon(void) { }
+#endif
+
+static pascal OSErr HandleQuitMessage(const AppleEvent* ev, AppleEvent* reply, long handlerRefcon) {
+	Window_RequestClose();
+	return 0;
+}
+
+static void DoCreateWindow(int width, int height) {
+	CCWindowDelegate* del;
+	NSRect rect;
+
+	// Technically the coordinates for the origin are at bottom left corner
+	// But since the window is in centre of the screen, don't need to care here
+	rect.origin.x    = Display_CentreX(width);  
+	rect.origin.y    = Display_CentreY(height);
+	rect.size.width  = width; 
+	rect.size.height = height;
+
+	winHandle = [CCWindow alloc];
+	[winHandle initWithContentRect:rect styleMask:WIN_MASK backing:NSBackingStoreBuffered defer:false];
+	[winHandle setAcceptsMouseMovedEvents:YES];
+	
+
+	scroll_debugging = Options_GetBool("scroll-debug", false);
+	// for quit buttons in dock and menubar
+	AEInstallEventHandler(kCoreEventClass, kAEQuitApplication,
+		NewAEEventHandlerUPP(HandleQuitMessage), 0, false);
+	
+	Window_Main.Exists = true;
+	Window_Main.Handle = winHandle;
+	// CGAssociateMouseAndMouseCursorPosition implicitly grabs cursor
+
+	del = [CCWindowDelegate alloc];
+	[winHandle setDelegate:del];
+	RefreshWindowBounds();
+	MakeContentView();
+	ApplyIcon();
+
+	canCheckOcclusion = [winHandle respondsToSelector:@selector(occlusionState)];
+}
+void Window_Create2D(int width, int height) { DoCreateWindow(width, height); }
+void Window_Create3D(int width, int height) { DoCreateWindow(width, height); }
+
+void Window_SetTitle(const cc_string* title) {
+	char raw[NATIVE_STR_LEN];
+	NSString* str;
+	String_EncodeUtf8(raw, title);
+
+	str = [NSString stringWithUTF8String:raw];
+	[winHandle setTitle:str];
+	[str release];
+}
+
+// NOTE: Only defined since macOS 10.7 SDK
+#define _NSFullScreenWindowMask (1 << 14)
+int Window_GetWindowState(void) {
+	int flags;
+
+	// modern fullscreen using toggleFullScreen
+	flags = [winHandle styleMask];
+	if (flags & _NSFullScreenWindowMask) return WINDOW_STATE_FULLSCREEN;
+
+	// legacy fullscreen using CGLSetFullscreen
+	if (legacy_fullscreen) return WINDOW_STATE_FULLSCREEN;
+
+	flags = [winHandle isMiniaturized];
+	return flags ? WINDOW_STATE_MINIMISED : WINDOW_STATE_NORMAL;
+}
+
+// NOTE: Only defined since macOS 10.9 SDK
+#define _NSWindowOcclusionStateVisible (1 << 1)
+int Window_IsObscured(void) {
+    if (!canCheckOcclusion)
+        return [winHandle isMiniaturized];
+    
+    // covers both minimised and hidden behind another window
+    int flags = [winHandle occlusionState];
+    return !(flags & _NSWindowOcclusionStateVisible);
+}
+
+void Window_Show(void) { 
+	[winHandle makeKeyAndOrderFront:appHandle];
+	RefreshWindowBounds(); // TODO: even necessary?
+}
+
+void Window_SetSize(int width, int height) {
+	// Can't use setContentSize:, because that resizes from the bottom left corner
+	NSRect rect = [winHandle frame];
+
+	rect.origin.y    += Window_Main.Height - height;
+	rect.size.width  += width  - Window_Main.Width;
+	rect.size.height += height - Window_Main.Height;
+	[winHandle setFrame:rect display:YES];
+}
+
+void Window_RequestClose(void) { 
+	[winHandle close];
+}
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------Event processing----------------------------------------------------*
+*#########################################################################################################################*/
+static int MapNativeMouse(long button) {
+	if (button == 0) return CCMOUSE_L;
+	if (button == 1) return CCMOUSE_R;
+	if (button == 2) return CCMOUSE_M;
+
+	if (button == 3) return CCMOUSE_X1;
+	if (button == 4) return CCMOUSE_X2;
+	if (button == 5) return CCMOUSE_X3;
+	if (button == 6) return CCMOUSE_X4;
+	if (button == 7) return CCMOUSE_X5;
+	if (button == 8) return CCMOUSE_X6;
+
+	Platform_Log1("Unknown mouse button: %i", &button);
+	return 0;
+}
+
+static void ProcessKeyChars(id ev) {
+	const char* src;
+	cc_codepoint cp;
+	NSString* chars;
+	int i, len, flags;
+
+	// Ignore text input while cmd is held down
+	// e.g. so Cmd + V to paste doesn't leave behind 'v'
+	flags = [ev modifierFlags];
+	if (flags & 0x000008) return;
+	if (flags & 0x000010) return;
+
+	chars = [ev characters];
+	src   = [chars UTF8String];
+	len   = String_Length(src);
+
+	while (len > 0) {
+		i = Convert_Utf8ToCodepoint(&cp, src, len);
+		if (!i) break;
+
+		Event_RaiseInt(&InputEvents.Press, cp);
+		src += i; len -= i;
+	}
+}
+
+static int TryGetKey(NSEvent* ev) {
+	int code = [ev keyCode];
+	int key  = MapNativeKey(code);
+	if (key) return key;
+
+	Platform_Log1("Unknown key %i", &code);
+	return 0;
+}
+
+static void DebugScrollEvent(NSEvent* ev) {
+#ifdef kCGScrollWheelEventDeltaAxis1
+	float dy = [ev deltaY];
+	int steps = dy > 0.0f ? Math_Ceil(dy) : Math_Floor(dy);
+	
+	CGEventRef ref = [ev CGEvent];
+	if (!ref) return;
+	int raw = CGEventGetIntegerValueField(ref, kCGScrollWheelEventDeltaAxis1);
+	
+	Platform_Log3("SCROLL: %i.0 = (%i, %f3)", &steps, &raw, &dy);
+#endif
+}
+
+void Window_ProcessEvents(float delta) {
+	NSEvent* ev;
+	int key, type, steps, x, y;
+	float dx, dy;
+	
+	// https://wiki.freepascal.org/Cocoa_Internals/Application 
+	[pool release];
+	pool = [[NSAutoreleasePool alloc] init];
+
+	for (;;) {
+		ev = [appHandle nextEventMatchingMask:ANY_EVENT_MASK untilDate:Nil inMode:NSDefaultRunLoopMode dequeue:YES];
+		if (!ev) break;
+		type = [ev type];
+
+		switch (type) {
+		case  1: // NSLeftMouseDown 
+		case  3: // NSRightMouseDown
+		case 25: // NSOtherMouseDown
+			key = MapNativeMouse([ev buttonNumber]);
+			if (GetMouseCoords(&x, &y) && key) Input_SetPressed(key);
+			break;
+
+		case  2: // NSLeftMouseUp 
+		case  4: // NSRightMouseUp
+		case 26: // NSOtherMouseUp
+			key = MapNativeMouse([ev buttonNumber]);
+			if (key) Input_SetReleased(key);
+			break;
+
+		case 10: // NSKeyDown
+			key = TryGetKey(ev);
+			if (key) Input_SetPressed(key);
+			// TODO: Test works properly with other languages
+			ProcessKeyChars(ev);
+			break;
+
+		case 11: // NSKeyUp
+			key = TryGetKey(ev);
+			if (key) Input_SetReleased(key);
+			break;
+
+		case 12: // NSFlagsChanged
+			key = [ev modifierFlags];
+			// TODO: Figure out how to only get modifiers that changed
+			Input_Set(CCKEY_LCTRL,    key & 0x000001);
+			Input_Set(CCKEY_LSHIFT,   key & 0x000002);
+			Input_Set(CCKEY_RSHIFT,   key & 0x000004);
+			Input_Set(CCKEY_LWIN,     key & 0x000008);
+			Input_Set(CCKEY_RWIN,     key & 0x000010);
+			Input_Set(CCKEY_LALT,     key & 0x000020);
+			Input_Set(CCKEY_RALT,     key & 0x000040);
+			Input_Set(CCKEY_RCTRL,    key & 0x002000);
+			Input_Set(CCKEY_CAPSLOCK, key & 0x010000);
+			break;
+
+		case 22: // NSScrollWheel
+			if (scroll_debugging) DebugScrollEvent(ev);
+			dx    = [ev deltaX];
+			dy    = [ev deltaY];
+			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=220175
+			//  delta is in 'line height' units, but I don't know how to map that to actual units.
+			// All I know is that scrolling by '1 wheel notch' produces a delta of around 0.1, and that
+			//  sometimes I'll see it go all the way up to 5-6 with a larger wheel scroll.
+			// So mulitplying by 10 doesn't really seem a good idea, instead I just round outwards.
+			//  TODO: Figure out if there's a better way than this. */	
+			steps = dx > 0.0f ? Math_Ceil(dx) : Math_Floor(dx);
+			Mouse_ScrollHWheel(steps);
+			
+			steps = dy > 0.0f ? Math_Ceil(dy) : Math_Floor(dy);
+			Mouse_ScrollVWheel(steps);
+			break;
+
+		case  5: // NSMouseMoved
+		case  6: // NSLeftMouseDragged
+		case  7: // NSRightMouseDragged
+		case 27: // NSOtherMouseDragged
+			if (GetMouseCoords(&x, &y)) Pointer_SetPosition(0, x, y);
+
+			if (Input.RawMode) ProcessRawMouseMovement(ev);
+			warping = false;
+			break;
+		}
+		[appHandle sendEvent:ev];
+	}
+}
+
+void Window_ProcessGamepads(float delta) { }
+
+
+/*########################################################################################################################*
+*-----------------------------------------------------------Dialogs-------------------------------------------------------*
+*#########################################################################################################################*/
+void ShowDialogCore(const char* title, const char* msg) {
+	CFStringRef titleCF, msgCF;
+	NSAlert* alert;
+	
+	titleCF = CFStringCreateWithCString(NULL, title, kCFStringEncodingASCII);
+	msgCF   = CFStringCreateWithCString(NULL, msg,   kCFStringEncodingASCII);
+	
+	alert = [NSAlert alloc];
+	alert = [alert init];
+	
+	[alert setMessageText: titleCF];
+	[alert setInformativeText: msgCF];
+	[alert addButtonWithTitle: @"OK"];
+	
+	[alert runModal];
+	CFRelease(titleCF);
+	CFRelease(msgCF);
+}
+
+static NSMutableArray* GetOpenSaveFilters(const char* const* filters) {
+    NSMutableArray* types = [NSMutableArray array];
+    int i;
+
+    for (i = 0; filters[i]; i++)
+    {
+        NSString* filter = [NSString stringWithUTF8String:filters[i]];
+        filter = [filter substringFromIndex:1];
+        [types addObject:filter];
+    }
+    return types;
+}
+
+static void OpenSaveDoCallback(NSURL* url, FileDialogCallback callback) {
+    NSString* str;
+    const char* src;
+    int len;
+    
+    str = [url path];
+    src = [str UTF8String];
+    len = String_Length(src);
+    
+    cc_string path; char pathBuffer[NATIVE_STR_LEN];
+    String_InitArray(path, pathBuffer);
+    String_AppendUtf8(&path, src, len);
+    callback(&path);
+}
+
+cc_result Window_SaveFileDialog(const struct SaveFileDialogArgs* args) {
+	NSSavePanel* dlg = [NSSavePanel savePanel];
+	
+	// TODO: Use args->defaultName, but only macOS 10.6
+
+    NSMutableArray* types = GetOpenSaveFilters(args->filters);
+    [dlg setAllowedFileTypes:types];
+	if ([dlg runModal] != DIALOG_OK) return 0;
+
+	NSURL* file = [dlg URL];
+    if (file) OpenSaveDoCallback(file, args->Callback);
+ 	return 0;
+}
+
+cc_result Window_OpenFileDialog(const struct OpenFileDialogArgs* args) {
+    NSOpenPanel* dlg = [NSOpenPanel openPanel];
+    
+    NSMutableArray* types = GetOpenSaveFilters(args->filters);
+    [dlg setCanChooseFiles: YES];
+    if ([dlg runModalForTypes:types] != DIALOG_OK) return 0;
+    // unfortunately below code doesn't work when linked against SDK < 10.6
+    //   https://developer.apple.com/documentation/appkit/nssavepanel/1534419-allowedfiletypes
+    // [dlg setAllowedFileTypes:types];
+    // if ([dlg runModal] != DIALOG_OK) return 0;
+    
+    NSArray* files = [dlg URLs];
+    if ([files count] < 1) return 0;
+    
+    NSURL* file = [files objectAtIndex:0];
+    OpenSaveDoCallback(file, args->Callback);
+    return 0;
+}
+
+
+/*########################################################################################################################*
+*--------------------------------------------------------Framebuffer------------------------------------------------------*
+*#########################################################################################################################*/
+static struct Bitmap fb_bmp;
+void Window_AllocFramebuffer(struct Bitmap* bmp, int width, int height) {
+	bmp->scan0  = (BitmapCol*)Mem_Alloc(width * height, 4, "window pixels");
+	bmp->width  = width;
+	bmp->height = height;
+	fb_bmp      = *bmp;
+}
+
+static void DoDrawFramebuffer(NSRect dirty) {
+	CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
+	CGContextRef context = NULL;
+	CGDataProviderRef provider;
+	NSGraphicsContext* nsContext;
+	CGImageRef image;
+	CGRect rect;
+	
+	Event_RaiseVoid(&WindowEvents.Redrawing);
+
+	// Unfortunately CGImageRef is immutable, so changing the
+	//  underlying data doesn't change what shows when drawing.
+	// TODO: Find a better way of doing this in cocoa..
+	if (!fb_bmp.scan0) return;
+	nsContext = [NSGraphicsContext currentContext];
+	context   = [nsContext graphicsPort];
+
+	// TODO: Only update changed bit..
+	rect.origin.x = 0; rect.origin.y = 0;
+	rect.size.width  = Window_Main.Width;
+	rect.size.height = Window_Main.Height;
+
+	// TODO: REPLACE THIS AWFUL HACK
+	provider = CGDataProviderCreateWithData(NULL, fb_bmp.scan0,
+		Bitmap_DataSize(fb_bmp.width, fb_bmp.height), NULL);
+	image = CGImageCreate(fb_bmp.width, fb_bmp.height, 8, 32, fb_bmp.width * 4, colorSpace,
+		kCGBitmapByteOrder32Host | kCGImageAlphaNoneSkipFirst, provider, NULL, 0, 0);
+
+	CGContextDrawImage(context, rect, image);
+	CGContextSynchronize(context);
+
+	CGImageRelease(image);
+	CGDataProviderRelease(provider);
+	CGColorSpaceRelease(colorSpace);
+}
+
+void Window_DrawFramebuffer(Rect2D r, struct Bitmap* bmp) {
+	NSRect rect;
+	rect.origin.x    = r.x; 
+	rect.origin.y    = Window_Main.Height - r.y - r.height;
+	rect.size.width  = r.width;
+	rect.size.height = r.height;
+	
+	[viewHandle setNeedsDisplayInRect:rect];
+	[viewHandle displayIfNeeded];
+}
+
+void Window_FreeFramebuffer(struct Bitmap* bmp) {
+	Mem_Free(bmp->scan0);
+}
+
+void OnscreenKeyboard_Open(struct OpenKeyboardArgs* args) { }
+void OnscreenKeyboard_SetText(const cc_string* text) { }
+void OnscreenKeyboard_Draw2D(Rect2D* r, struct Bitmap* bmp) { }
+void OnscreenKeyboard_Draw3D(void) { }
+void OnscreenKeyboard_Close(void) { }
+
+
+/*########################################################################################################################*
+*--------------------------------------------------------NSOpenGL---------------------------------------------------------*
+*#########################################################################################################################*/
+#if (CC_GFX_BACKEND & CC_GFX_BACKEND_GL_MASK) && !defined CC_BUILD_EGL
+static NSOpenGLContext* ctxHandle;
+#include <OpenGL/OpenGL.h>
+
+// SDKs < macOS 10.7 do not have this defined
+#ifndef kCGLRPVideoMemoryMegabytes
+#define kCGLRPVideoMemoryMegabytes 131
+#endif
+
+static int SupportsModernFullscreen(void) {
+	return [winHandle respondsToSelector:@selector(toggleFullScreen:)];
+}
+
+static NSOpenGLPixelFormat* MakePixelFormat(cc_bool fullscreen) {
+	// TODO: Is there a penalty for fullscreen contexts in 10.7 and later?
+	// Need to test whether there is a performance penalty or not
+	if (SupportsModernFullscreen()) fullscreen = false;
+	
+	NSOpenGLPixelFormatAttribute attribs[] = {
+		NSOpenGLPFAColorSize,    DisplayInfo.Depth,
+		NSOpenGLPFADepthSize,    24,
+		NSOpenGLPFADoubleBuffer,
+		fullscreen ? NSOpenGLPFAFullScreen : 0,
+		// TODO do we have to mask to main display? or can we just use -1 for all displays?
+		NSOpenGLPFAScreenMask,   CGDisplayIDToOpenGLDisplayMask(CGMainDisplayID()),
+		0
+	};
+	return [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs];
+}
+
+void GLContext_Create(void) {
+	NSOpenGLPixelFormat* fmt;
+	fmt = MakePixelFormat(true);
+	if (!fmt) {
+		Platform_LogConst("Failed to create full screen pixel format.");
+		Platform_LogConst("Trying again to create a non-fullscreen pixel format.");
+		fmt = MakePixelFormat(false);
+	}
+	if (!fmt) Logger_Abort("Choosing pixel format");
+
+	ctxHandle = [NSOpenGLContext alloc];
+	ctxHandle = [ctxHandle initWithFormat:fmt shareContext:Nil];
+	if (!ctxHandle) Logger_Abort("Failed to create OpenGL context");
+
+	[ctxHandle setView:viewHandle];
+	[fmt release];
+	[ctxHandle makeCurrentContext];
+	[ctxHandle update];
+}
+
+void GLContext_Update(void) {
+	// TODO: Why does this crash on resizing
+	[ctxHandle update];
+}
+cc_bool GLContext_TryRestore(void) { return true; }
+
+void GLContext_Free(void) { 
+	[NSOpenGLContext clearCurrentContext];
+	[ctxHandle clearDrawable];
+	[ctxHandle release];
+}
+
+void* GLContext_GetAddress(const char* function) {
+	static const cc_string glPath = String_FromConst("/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL");
+	static void* lib;
+
+	if (!lib) lib = DynamicLib_Load2(&glPath);
+	return DynamicLib_Get2(lib, function);
+}
+
+cc_bool GLContext_SwapBuffers(void) {
+	[ctxHandle flushBuffer];
+	return true;
+}
+
+void GLContext_SetFpsLimit(cc_bool vsync, float minFrameMs) {
+	int value = vsync ? 1 : 0;
+	[ctxHandle setValues:&value forParameter: NSOpenGLCPSwapInterval];
+}
+
+// kCGLCPCurrentRendererID is only available on macOS 10.4 and later
+// Before 10.5 uses long instead of glInt and didn't include the normal gl.h with typedefs
+#if defined MAC_OS_X_VERSION_10_4
+typedef int GLinteger;
+
+static const char* GetAccelerationMode(CGLContextObj ctx) {
+	GLinteger fGPU, vGPU;
+	
+	// NOTE: only macOS 10.4 or later
+	if (CGLGetParameter(ctx, kCGLCPGPUFragmentProcessing, &fGPU)) return NULL;
+	if (CGLGetParameter(ctx, kCGLCPGPUVertexProcessing,   &vGPU)) return NULL;
+	
+	if (fGPU && vGPU) return "Fully";
+	if (fGPU || vGPU) return "Partially";
+	return "Not";
+}
+
+void GLContext_GetApiInfo(cc_string* info) {
+	CGLContextObj ctx = [ctxHandle CGLContextObj];
+	GLinteger rendererID;
+	CGLGetParameter(ctx, kCGLCPCurrentRendererID, &rendererID);
+	
+	GLinteger nRenders = 0;
+	CGLRendererInfoObj rend;
+	CGLQueryRendererInfo(-1, &rend, &nRenders);
+	int i;
+	
+	for (i = 0; i < nRenders; i++)
+	{
+		GLinteger curID = -1;
+		CGLDescribeRenderer(rend, i, kCGLRPRendererID, &curID);
+		if (curID != rendererID) continue;
+		
+		GLinteger acc = 0;
+		CGLDescribeRenderer(rend, i, kCGLRPAccelerated, &acc);
+		const char* mode = GetAccelerationMode(ctx);
+		
+		GLinteger vram = 0;
+		if (!CGLDescribeRenderer(rend, i, kCGLRPVideoMemoryMegabytes, &vram)) {
+			// preferred path (macOS 10.7 or later)
+		} else if (!CGLDescribeRenderer(rend, i, kCGLRPVideoMemory, &vram)) {
+			vram /= (1024 * 1024); // TODO: use float instead?
+		} else {
+			vram = -1; // TODO show a better error?
+		}
+		
+		if (mode && acc) {
+			String_Format2(info, "VRAM: %i MB, %c HW accelerated\n", &vram, mode);
+		} else {
+			String_Format2(info, "VRAM: %i MB, %c\n",
+						   &vram, acc ? "HW accelerated" : "no HW acceleration");
+		}
+		break;
+	}
+	CGLDestroyRendererInfo(rend);
+}
+#else
+// macOS 10.3 and earlier case
+void GLContext_GetApiInfo(cc_string* info) {
+	// TODO: retrieve rendererID from a CGLPixelFormatObj, but this isn't all that important
+}
+#endif
+
+cc_result Window_EnterFullscreen(void) {
+	if (SupportsModernFullscreen()) {
+		[winHandle toggleFullScreen:appHandle];
+		return 0;
+	}
+
+	Platform_LogConst("Falling back to legacy fullscreen..");
+	legacy_fullscreen = true;
+	[ctxHandle clearDrawable];
+	CGDisplayCapture(CGMainDisplayID());
+
+	// setFullScreen doesn't return an error code, which is unfortunate
+	//  because if setFullScreen fails, you're left with a blank window
+	//  that's still rendering thousands of frames per second
+	//[ctxHandle setFullScreen];
+	//return 0;
+	
+	// CGLSetFullScreenOnDisplay is the preferable API, because it  
+	//  works properly on macOS 10.7 and all later versions
+	// However, because this API was only introduced in 10.7, it 
+	//  is essentially useless for us - because the superior 
+	//  toggleFullScreen API is already used in macOS 10.7+
+	//cc_result res = CGLSetFullScreenOnDisplay([ctxHandle CGLContextObj], CGDisplayIDToOpenGLDisplayMask(CGMainDisplayID()));
+	
+	// CGLSetFullsScreen has existed since macOS 10.1, however
+	//  it was deprecated in 10.6 - and by deprecated, Apple
+	//  REALLY means deprecated. If the SDK ClassiCube is compiled
+	//  against is 10.6 or later, then CGLSetFullScreen will always
+	//  fail to work (CGLSetFullScreenOnDisplay still works) though
+	// So make sure you compile ClassiCube with an older SDK version
+	cc_result res = CGLSetFullScreen([ctxHandle CGLContextObj]);
+
+	if (res) Window_ExitFullscreen();
+	RefreshWindowBounds();
+	Event_RaiseVoid(&WindowEvents.Resized);
+	return res;
+}
+
+cc_result Window_ExitFullscreen(void) {
+	if (SupportsModernFullscreen()) {
+		[winHandle toggleFullScreen:appHandle];
+		return 0;
+	}
+
+	legacy_fullscreen = false;
+	CGDisplayRelease(CGMainDisplayID());
+	[ctxHandle clearDrawable];
+	[ctxHandle setView:viewHandle];
+
+	RefreshWindowBounds();
+	Event_RaiseVoid(&WindowEvents.Resized);
+	return 0;
+}
+#endif
+#endif