#include "OSXWindow.h"
#include "OSXWindowFrameView.h"
#include <Cocoa/Cocoa.h>
#include <Carbon/Carbon.h>
#include <MetalKit/MetalKit.h>
#include <unistd.h>
extern id<MTLCommandQueue> g_command_queue;
extern id<MTLLibrary> g_library;
extern id<MTLRenderPipelineState> g_pipeline_state;
id<MTLDevice> g_metal_device;
NSString* g_shadersSrc = @
"#include <metal_stdlib>\n"
"using namespace metal;\n"
"struct VertexOutput {\n"
"float4 pos [[position]];\n"
"float2 texcoord;\n"
"};\n"
"vertex VertexOutput vertFunc(\n"
"unsigned int vID[[vertex_id]],\n"
"constant float4* vertexArray[[buffer(0)]])\n"
"{\n"
"VertexOutput out;\n"
"out.pos.x = (vertexArray[vID].x * 2.0) - 1.0;\n"
"out.pos.y = (vertexArray[vID].y * 2.0) - 1.0;\n"
"out.pos.z = 0.0;\n"
"out.pos.w = 1.0;\n"
"out.texcoord.x = vertexArray[vID].w;\n"
"out.texcoord.y = vertexArray[vID].z;\n"
"return out;\n"
"}\n"
"fragment float4 fragFunc(VertexOutput input [[stage_in]],\n"
"texture2d<half> colorTexture [[ texture(0) ]])\n"
"{\n"
"constexpr sampler textureSampler(mag_filter::nearest, min_filter::nearest);\n"
"const half4 colorSample = colorTexture.sample(textureSampler, input.texcoord);\n"
"return float4(colorSample);\n"
"}\n";
static bool s_init = false;
const uint32_t WINDOW_BORDERLESS = 1 << 1;
const uint32_t WINDOW_RESIZE = 1 << 2;
const uint32_t WINDOW_TITLE = 1 << 3;
static void create_standard_menu();
enum CursorStyle {
CursorStyle_Arrow,
CursorStyle_Ibeam,
CursorStyle_Crosshair,
CursorStyle_ClosedHand,
CursorStyle_OpenHand,
CursorStyle_ResizeLeftRight,
CursorStyle_ResizeUpDown,
CursorStyle_SizeAll,
CursorStyle_Count,
};
static NSCursor* s_cursors[CursorStyle_Count];
void cursor_init() {
s_cursors[CursorStyle_Arrow] = [[NSCursor arrowCursor] retain];
s_cursors[CursorStyle_Ibeam] = [[NSCursor IBeamCursor] retain];
s_cursors[CursorStyle_Crosshair] = [[NSCursor crosshairCursor] retain];
s_cursors[CursorStyle_ClosedHand] = [[NSCursor closedHandCursor] retain];
s_cursors[CursorStyle_OpenHand] = [[NSCursor openHandCursor] retain];
s_cursors[CursorStyle_ResizeLeftRight] = [[NSCursor resizeLeftRightCursor] retain];
s_cursors[CursorStyle_ResizeUpDown] = [[NSCursor resizeUpDownCursor] retain];
s_cursors[CursorStyle_SizeAll] = [[NSCursor closedHandCursor] retain];
}
#ifdef __clang__
#pragma clang diagnostic ignored "-Wobjc-method-access"
#endif
static bool create_shaders() {
NSError* nsError = NULL;
NSError** nsErrorPtr = &nsError;
id<MTLLibrary> library = [g_metal_device newLibraryWithSource:g_shadersSrc
options:[[MTLCompileOptions alloc] init]
error:nsErrorPtr];
if (nsError || !library) {
NSLog(@"Unable to create shaders %@", nsError);
return false;
}
g_library = library;
id<MTLFunction> vertex_shader_func = [g_library newFunctionWithName:@"vertFunc"];
id<MTLFunction> fragment_shader_func = [g_library newFunctionWithName:@"fragFunc"];
if (!vertex_shader_func) {
printf("Unable to get vertFunc!\n");
return false;
}
if (!fragment_shader_func) {
printf("Unable to get fragFunc!\n");
return false;
}
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.label = @"MyPipeline";
pipelineStateDescriptor.vertexFunction = vertex_shader_func;
pipelineStateDescriptor.fragmentFunction = fragment_shader_func;
pipelineStateDescriptor.colorAttachments[0].pixelFormat = 80;
NSError *error = NULL;
g_pipeline_state = [g_metal_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
if (!g_pipeline_state) {
NSLog(@"Failed to created pipeline state, error %@", error);
}
return true;
}
void* mfb_open(const char* name, int width, int height, uint32_t flags, int scale, void** view_handle) {
bool prev_init = s_init;
if (!s_init) {
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
create_standard_menu();
cursor_init();
g_metal_device = MTLCreateSystemDefaultDevice();
if (!g_metal_device) {
printf("Your device/OS doesn't support Metal.");
return 0;
}
if (!create_shaders()) {
return 0;
}
s_init = true;
}
NSWindowStyleMask styles = NSWindowStyleMaskClosable;
if (flags & WINDOW_BORDERLESS) {
styles |= NSWindowStyleMaskBorderless;
}
if (flags & WINDOW_RESIZE) {
styles |= NSWindowStyleMaskResizable;
}
if (flags & WINDOW_TITLE) {
styles |= NSWindowStyleMaskTitled;
}
NSRect rectangle = NSMakeRect(0, 0, width * scale, (height * scale));
OSXWindow* window = [[OSXWindow alloc] initWithContentRect:rectangle styleMask:styles backing:NSBackingStoreBuffered defer:NO];
if (!window) {
return 0;
}
window->draw_parameters = calloc(sizeof(DrawParameters), 1);
if (!window->draw_parameters) {
return 0;
}
g_command_queue = [g_metal_device newCommandQueue];
WindowViewController* viewController = [WindowViewController new];
MTLTextureDescriptor* textureDescriptor = [[MTLTextureDescriptor alloc] init];
textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
textureDescriptor.width = width;
textureDescriptor.height = height;
const float scale_t = 1.00f;
const float uv_const = 1.0f;
static const Vertex intial_vertices[] = {
{ -1.0f * scale_t, 1.0f * scale_t, 0.0f, 0.0f },
{ 1.0f * scale_t, 1.0f * scale_t, uv_const, 0.0f },
{ 1.0f * scale_t, -1.0f * scale_t, uv_const, uv_const },
{ -1.0f * scale_t, 1.0f * scale_t, 0.0f, 0.0f },
{ 1.0f * scale_t, -1.0f * scale_t, uv_const, uv_const },
{ -1.0f * scale_t, -1.0f * scale_t, 0.0f, uv_const },
};
viewController->m_width = width;
viewController->m_height = height;
for (int i = 0; i < MaxBuffersInFlight; ++i) {
viewController->m_draw_state[i].texture_width = width;
viewController->m_draw_state[i].texture_height = height;
viewController->m_draw_state[i].texture =
[g_metal_device newTextureWithDescriptor:textureDescriptor];
viewController->m_draw_state[i].vertex_buffer =
[g_metal_device newBufferWithBytes:intial_vertices
length:sizeof(intial_vertices)
options:MTLResourceStorageModeShared];
viewController->m_delayed_delete_textures[i].frame_count = -1;
}
viewController->m_semaphore = dispatch_semaphore_create(MaxBuffersInFlight);
viewController->m_draw_parameters = window->draw_parameters;
MTKView* view = [[MTKView alloc] initWithFrame:rectangle];
view.device = g_metal_device;
view.delegate = viewController;
view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[window.contentView addSubview:view];
OSXWindowFrameView* temp_view = window->frame_view;
temp_view->m_view_controller = viewController;
window->width = width;
window->height = height;
window->scale = scale;
window->key_callback = 0;
window->shared_data = 0;
window->active_menu_id = -1;
window->prev_cursor = 0;
window->menu_data = malloc(sizeof(MenuData));
memset(window->menu_data, 0, sizeof(MenuData));
[window updateSize];
[window setTitle:[NSString stringWithUTF8String:name]];
[window setReleasedWhenClosed:NO];
[window performSelectorOnMainThread:@selector(makeKeyAndOrderFront:) withObject:nil waitUntilDone:YES];
[window setAcceptsMouseMovedEvents:YES];
[window center];
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp activateIgnoringOtherApps:YES];
});
if (!prev_init) {
[NSApp finishLaunching];
}
*view_handle = (void*)view;
return window;
}
void mfb_topmost(void* window, bool topmost) {
OSXWindow* win = (OSXWindow*)window;
if(topmost) {
win.level = NSFloatingWindowLevel; } else {
win.level = 0; }
}
static NSString* findAppName(void) {
size_t i;
NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString* GLFWNameKeys[] = {
@"CFBundleDisplayName",
@"CFBundleName",
@"CFBundleExecutable",
};
for (i = 0; i < sizeof(GLFWNameKeys) / sizeof(GLFWNameKeys[0]); i++) {
id name = [infoDictionary objectForKey:GLFWNameKeys[i]];
if (name &&
[name isKindOfClass:[NSString class]] &&
![name isEqualToString:@""])
{
return name;
}
}
extern char** _NSGetProgname();
char* progname = *_NSGetProgname();
if (progname) {
return [NSString stringWithUTF8String:progname];
}
return @"Unknown";
}
static void create_standard_menu(void) {
NSString* appName = findAppName();
NSMenu* bar = [[NSMenu alloc] init];
[NSApp setMainMenu:bar];
NSMenuItem* appMenuItem =
[bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
NSMenu* appMenu = [[NSMenu alloc] init];
[appMenuItem setSubmenu:appMenu];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"About %@", appName]
action:@selector(orderFrontStandardAboutPanel:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
NSMenu* servicesMenu = [[NSMenu alloc] init];
[NSApp setServicesMenu:servicesMenu];
[[appMenu addItemWithTitle:@"Services"
action:NULL
keyEquivalent:@""] setSubmenu:servicesMenu];
[servicesMenu release];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", appName]
action:@selector(hide:)
keyEquivalent:@"h"];
[[appMenu addItemWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:)
keyEquivalent:@"h"]
setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand];
[appMenu addItemWithTitle:@"Show All"
action:@selector(unhideAllApplications:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", appName]
action:@selector(terminate:)
keyEquivalent:@"q"];
}
void mfb_set_title(void* window, const char* title) {
OSXWindow* win = (OSXWindow*)window;
NSString* ns_title = [NSString stringWithUTF8String: title];
[win setTitle: ns_title];
}
void mfb_set_cursor_visibility(void *window, bool visibility) {
if (visibility) {
[NSCursor unhide];
} else {
[NSCursor hide];
}
}
void mfb_close(void* win) {
NSWindow* window = (NSWindow*)win;
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
if (window) {
[window close];
}
[pool drain];
}
static int update_events() {
NSEvent* event;
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
do {
event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES];
if (event) {
[NSApp sendEvent:event];
}
} while (event);
[pool release];
return 0;
}
static int generic_update(OSXWindow* win) {
if (win->shared_data) {
win->shared_data->scroll_x = 0.0f;
win->shared_data->scroll_y = 0.0f;
}
int state = update_events();
if (win->shared_data) {
NSPoint p = [win mouseLocationOutsideOfEventStream];
NSRect originalFrame = [win frame];
NSRect contentRect = [NSWindow contentRectForFrameRect: originalFrame styleMask: NSWindowStyleMaskTitled];
win->shared_data->mouse_x = p.x;
win->shared_data->mouse_y = contentRect.size.height - p.y;
}
return state;
}
int mfb_update(void* window, void* buffer) {
(void)buffer;
OSXWindow* win = (OSXWindow*)window;
return generic_update(win);
}
int mfb_update_with_buffer(void* window, void* buffer, uint32_t buf_width, uint32_t buf_height, uint32_t buf_stride) {
OSXWindow* win = (OSXWindow*)window;
win->draw_parameters->buffer_width = buf_width;
win->draw_parameters->buffer_height = buf_height;
win->draw_parameters->buffer_stride = buf_stride;
win->draw_parameters->scale_mode = 0;
if (win->shared_data) {
win->draw_parameters->buffer = buffer;
win->draw_parameters->bg_color = win->shared_data->bg_color;
win->draw_parameters->scale_mode = win->shared_data->scale_mode;
} else {
win->draw_parameters->scale_mode = 0;
}
int state = generic_update(win);
[[win contentView] setNeedsDisplay:YES];
return state;
}
static float transformY(float y) {
float b = CGDisplayBounds(CGMainDisplayID()).size.height;
float t = b - y;
return t;
}
void mfb_set_position(void* window, int x, int y) {
OSXWindow* win = (OSXWindow*)window;
const NSRect contentRect = [[win contentView] frame];
const NSRect dummyRect = NSMakeRect(x, transformY(y + contentRect.size.height), 0, 0);
const NSRect frameRect = [win frameRectForContentRect:dummyRect];
[win setFrameOrigin:frameRect.origin];
}
void mfb_get_position(const void* window, int *px, int *py) {
OSXWindow* win = (OSXWindow*)window;
const NSRect rectW = [win frame];
if (px != NULL) {
*px = rectW.origin.x;
}
if (py != NULL) {
const NSScreen *screen = [win screen];
const NSRect wsf = [screen frame];
const float height = wsf.size.height;
const float h = rectW.size.height;
*py = height - ( rectW.origin.y + h ); }
}
int mfb_should_close(void* window) {
OSXWindow* win = (OSXWindow*)window;
return win->should_close;
}
uint32_t mfb_get_screen_size() {
NSRect e = [[NSScreen mainScreen] frame];
uint32_t w = (uint32_t)e.size.width;
uint32_t h = (uint32_t)e.size.height;
return (w << 16) | h;
}
void mfb_set_key_callback(
void* window, void* rust_data,
void (*key_callback)(void* user_data, int key, int state),
void (*char_callback)(void* user_data, uint32_t key)
) {
OSXWindow* win = (OSXWindow*)window;
win->key_callback = key_callback;
win->char_callback = char_callback;
win->rust_data = rust_data;
}
void mfb_set_mouse_data(void* window, SharedData* shared_data) {
OSXWindow* win = (OSXWindow*)window;
win->shared_data = shared_data;
}
void mfb_set_cursor_style(void* window, int cursor) {
OSXWindow* win = (OSXWindow*)window;
if (win->prev_cursor == cursor) {
return;
}
if (cursor < 0 || cursor >= CursorStyle_Count) {
printf("cursor out of range %d\n", cursor);
return;
}
[s_cursors[cursor] set];
win->prev_cursor = cursor;
}
uint32_t mfb_is_active(void* window) {
OSXWindow* win = (OSXWindow*)window;
return win->is_active;
}
void mfb_remove_menu(void* window, const char* name) {
OSXWindow* win = (OSXWindow*)window;
NSMenu* main_menu = [NSApp mainMenu];
int len = win->menu_data->menu_count;
for (int i = 0; i < len; ++i) {
Menu* menu = &win->menu_data->menus[i];
if (strcmp(menu->name, name)) {
continue;
}
Menu* menu_end = &win->menu_data->menus[len - 1];
[main_menu removeItem:menu->menu_item];
menu->name = menu_end->name;
menu->menu = menu_end->menu;
menu->menu_item = menu_end->menu_item;
win->menu_data->menu_count--;
}
}
int mfb_active_menu(void* window) {
OSXWindow* win = (OSXWindow*)window;
int active_menu_id = win->active_menu_id;
win->active_menu_id = -1;
return active_menu_id;
}
static CFStringRef create_string_for_key(CGKeyCode keyCode) {
TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
CFDataRef layoutData = TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
if (!layoutData) {
return 0;
}
const UCKeyboardLayout* keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(layoutData);
UInt32 keysDown = 0;
UniChar chars[4];
UniCharCount realLength;
UCKeyTranslate(keyboardLayout,
keyCode,
kUCKeyActionDisplay,
0,
LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit,
&keysDown,
sizeof(chars) / sizeof(chars[0]),
&realLength,
chars);
CFRelease(currentKeyboard);
return CFStringCreateWithCharacters(kCFAllocatorDefault, chars, 1);
}
static NSString* convert_key_code_to_string(int key) {
if (key < 128) {
NSString* charName = (NSString*)create_string_for_key(key);
if (charName) {
return charName;
}
return [NSString stringWithFormat:@"%c", (char)key];
}
return [NSString stringWithFormat:@"%C", (uint16_t)key];
}
const uint32_t MENU_KEY_COMMAND = 1;
const uint32_t MENU_KEY_WIN = 2;
const uint32_t MENU_KEY_SHIFT= 4;
const uint32_t MENU_KEY_CTRL = 8;
const uint32_t MENU_KEY_ALT = 16;
static NSString* get_string_for_key(uint32_t t) {
unichar c = (unichar)t;
NSString* key = [NSString stringWithCharacters:&c length:1];
return key;
}
uint64_t mfb_add_menu_item(
void* in_menu,
int32_t menu_id,
const char* item_name,
bool enabled,
uint32_t key,
uint32_t modfier
) {
NSMenu* menu = (NSMenu*)in_menu;
NSString* name = [NSString stringWithUTF8String: item_name];
if (menu_id == -1) {
[menu addItem:[NSMenuItem separatorItem]];
} else {
NSString* key_string = 0;
int mask = 0;
NSMenuItem* newItem = [[NSMenuItem alloc] initWithTitle:name action:@selector(onMenuPress:) keyEquivalent:@""];
[newItem setTag:menu_id];
if ((modfier & MENU_KEY_CTRL)) {
mask |= NSEventModifierFlagCommand;
}
if ((modfier & MENU_KEY_CTRL) && (modfier & MENU_KEY_COMMAND)) {
mask |= NSEventModifierFlagControl;
}
if (modfier & MENU_KEY_SHIFT) {
mask |= NSEventModifierFlagShift;
}
if (modfier & MENU_KEY_ALT) {
mask |= NSEventModifierFlagOption;
}
switch (key) {
case 0x7a: { key_string = get_string_for_key(NSF1FunctionKey); break; } case 0x78: { key_string = get_string_for_key(NSF2FunctionKey); break; } case 0x63: { key_string = get_string_for_key(NSF3FunctionKey); break; } case 0x76: { key_string = get_string_for_key(NSF4FunctionKey); break; } case 0x60: { key_string = get_string_for_key(NSF5FunctionKey); break; } case 0x61: { key_string = get_string_for_key(NSF6FunctionKey); break; } case 0x62: { key_string = get_string_for_key(NSF7FunctionKey); break; } case 0x64: { key_string = get_string_for_key(NSF8FunctionKey); break; } case 0x65: { key_string = get_string_for_key(NSF9FunctionKey); break; } case 0x6d: { key_string = get_string_for_key(NSF10FunctionKey); break; } case 0x67: { key_string = get_string_for_key(NSF11FunctionKey); break; } case 0x6f: { key_string = get_string_for_key(NSF12FunctionKey); break; } case 0x7f: break;
default: {
key_string = convert_key_code_to_string(key);
}
}
if (key_string) {
[newItem setKeyEquivalentModifierMask: mask];
[newItem setKeyEquivalent:key_string];
}
if (enabled) {
[newItem setEnabled:YES];
} else {
[newItem setEnabled:NO];
}
[newItem setOnStateImage: newItem.offStateImage];
[menu addItem:newItem];
return (uint64_t)newItem;
}
return 0;
}
void mfb_add_sub_menu(void* parent_menu, const char* menu_name, void* attach_menu) {
NSMenu* parent = (NSMenu*)parent_menu;
NSMenu* attach = (NSMenu*)attach_menu;
[attach setAutoenablesItems:NO];
NSString* name = [NSString stringWithUTF8String: menu_name];
NSMenuItem* newItem = [[NSMenuItem alloc] initWithTitle:name action:NULL keyEquivalent:@""];
[newItem setSubmenu:attach];
[parent addItem:newItem];
}
void* mfb_create_menu(const char* name) {
NSString* ns_name = [NSString stringWithUTF8String: name];
NSMenu* menu = [[NSMenu alloc] initWithTitle:ns_name];
return (void*)menu;
}
void mfb_destroy_menu(void* menu_item, const char* name) {
(void)name;
NSMenuItem* item = (NSMenuItem*)menu_item;
NSMenu* main_menu = [NSApp mainMenu];
[main_menu removeItem:item];
}
void mfb_remove_menu_item(void* parent, uint64_t menu_item) {
NSMenu* menu = (NSMenu*)parent;
NSMenuItem* item = (NSMenuItem*)(uintptr_t)menu_item;
[menu removeItem:item];
}
uint64_t mfb_add_menu(void* window, void* m) {
(void)m;
(void)window;
NSMenu* menu = (NSMenu*)m;
NSMenu* main_menu = [NSApp mainMenu];
[menu setAutoenablesItems:NO];
NSMenuItem* windowMenuItem = [main_menu addItemWithTitle:@"" action:NULL keyEquivalent:@""];
[NSApp setWindowsMenu:menu];
[windowMenuItem setSubmenu:menu];
return (uint64_t)menu;
}
void mfb_remove_menu_at(void* window, int index) {
(void)window;
NSMenu* main_menu = [NSApp mainMenu];
[main_menu removeItemAtIndex:index];
}