Newer
Older
Import / research / match3 / windowing.cpp
//  C/C++ API for Mac OS X Windowing - windowing.cpp
//  Created by John Ryland (jryland@xiaofrog.com), 29/10/2017
//  Copyright (c) 2017 InvertedLogic
//  All rights reserved.
#include <cstdlib>
#include <cstdio>
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/message.h>
#include <objc/runtime.h>
#include <ApplicationServices/ApplicationServices.h>
#include <OpenGL/gl.h>
#include "windowing.h"
#include "common.h"

struct NSEvent;
struct NSPoint { double x, y; };
struct NSSize { double width, height; };
struct NSRect { double x, y, width, height; };

BOOL AppDidFinishLaunching(id self, SEL _cmd, id notification);
void WindowOnDraw(id self, SEL _cmd, NSRect rect);
void WindowOnResize(id self, SEL _cmd, NSSize newSize);
void WindowOnMouseEvent(id self, SEL _cmd, NSEvent* event);
void WindowOnTimerEvent(id self, SEL _cmd, void* param);

Application::Application()
{
  Class AppDelClass;
  AppDelClass = objc_allocateClassPair((Class)objc_getClass("NSObject"), "AppDelegate", 0);
  class_addMethod(AppDelClass, sel_getUid("applicationDidFinishLaunching:"), (IMP)AppDidFinishLaunching, "i@:@");
  class_addIvar(AppDelClass, "ApplicationPtr", sizeof(Application*), sizeof(Application*), "Application*");
  objc_registerClassPair(AppDelClass);
  Class ViewClass;
#if USE_OPENGL
  ViewClass = objc_allocateClassPair((Class)objc_getClass("NSOpenGLView"), "View", 0);
#else
  ViewClass = objc_allocateClassPair((Class)objc_getClass("NSView"), "View", 0);
#endif
  class_addMethod(ViewClass, sel_getUid("drawRect:"), (IMP)WindowOnDraw, "v@:");
  class_addMethod(ViewClass, sel_getUid("setFrameSize:"), (IMP)WindowOnResize, "v@:");
  class_addMethod(ViewClass, sel_getUid("mouseDown:"), (IMP)WindowOnMouseEvent, "v@:");
  class_addMethod(ViewClass, sel_getUid("mouseDragged:"), (IMP)WindowOnMouseEvent, "v@:");
  class_addMethod(ViewClass, sel_getUid("mouseUp:"), (IMP)WindowOnMouseEvent, "v@:");
  class_addMethod(ViewClass, sel_getUid("mouseMoved:"), (IMP)WindowOnMouseEvent, "v@:");
  class_addMethod(ViewClass, sel_getUid("processTimerEvent:"), (IMP)WindowOnTimerEvent, "v@:");
  class_addIvar(ViewClass, "WindowPtr", sizeof(Window*), sizeof(Window*), "Window*");
  objc_registerClassPair(ViewClass);
  objc_msgSend((id)objc_getClass("NSApplication"), sel_getUid("sharedApplication"));
}

int Application::run()
{
  extern id NSApp;
  id pool = objc_msgSend((id)objc_getClass("NSAutoreleasePool"), sel_getUid("alloc"));
  pool = objc_msgSend(pool, sel_getUid("init"));
  id appDelObj = objc_msgSend((id)objc_getClass("AppDelegate"), sel_getUid("alloc"));
  appDelObj = objc_msgSend(appDelObj, sel_getUid("init"));
  object_setInstanceVariable(appDelObj, "ApplicationPtr", (void*)this);
  objc_msgSend(NSApp, sel_getUid("setDelegate:"), appDelObj);
  objc_msgSend(NSApp, sel_getUid("run"));
  objc_msgSend(pool, sel_getUid("release"));
  return EXIT_SUCCESS;
}


void Window::create(int w, int h)
{
  // NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask | NSMiniaturizableWindowMask | NSWindowStyleMaskFullSizeContentView
  unsigned windowFlags = (1<<0) | (1<<1) | (1<<2) | (1<<3) | (1<<15);
  Window::onResize(w, h);
  window = objc_msgSend((id)objc_getClass("NSWindow"), sel_getUid("alloc"));
  window = objc_msgSend((id)window, sel_getUid("initWithContentRect:styleMask:backing:defer:"), (NSRect){0,0,w,h}, windowFlags, 2, false);
  view = objc_msgSend(objc_msgSend((id)objc_getClass("View"), sel_getUid("alloc")), sel_getUid("initWithFrame:"), (NSRect){0,0,w,h});
  object_setInstanceVariable((id)view, "WindowPtr", (void*)this);
  objc_msgSend((id)window, sel_getUid("setContentView:"), view);
  objc_msgSend((id)window, sel_getUid("becomeFirstResponder"));
  objc_msgSend((id)window, sel_getUid("makeKeyAndOrderFront:"), window);

// Enable retina
#if USE_RETINA
  objc_msgSend((id)view, sel_getUid("setWantsBestResolutionOpenGLSurface:"), YES);
#endif

  // Using timer events to drive the refresh
  objc_msgSend((id)objc_getClass("NSTimer"), sel_getUid("scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"), 
      0.020, view, sel_getUid("processTimerEvent:"), nil, YES);
}

Window::~Window()
{
}

void Window::flush()
{
#if USE_OPENGL
  id ctx = objc_msgSend((id)view, sel_getUid("openGLContext"));
  objc_msgSend(ctx, sel_getUid("makeCurrentContext"));
  glClearColor(0, 0, 0, 0);
  glClear(GL_COLOR_BUFFER_BIT);
  glDrawPixels(buffer.w, buffer.h, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, buffer.pixels.data());
  glFlush();
  objc_msgSend(ctx, sel_getUid("flushBuffer"));
#else
  CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
  CGContextRef bitmapContext = CGBitmapContextCreate(buffer.pixels.data(), buffer.w, buffer.h, 8, 4*buffer.w, colorSpace, kCGImageAlphaNoneSkipLast);
  CFRelease(colorSpace);
  CGImageRef imageRef = CGBitmapContextCreateImage(bitmapContext);
  id gfxContext = objc_msgSend((id)objc_getClass("NSGraphicsContext"), sel_getUid("currentContext"));
  // graphicsPort is deprecated and since 10.10, docs say the new API is CGContext
  CGContextRef ctx = (CGContextRef)objc_msgSend(gfxContext, sel_getUid("graphicsPort"));
  CGContextDrawImage(ctx, CGRectMake(0, 0, buffer.w, buffer.h), imageRef);
  CGImageRelease(imageRef);
#endif
}

void Window::onResize(int w, int h)
{
// Enable retina
#if USE_RETINA
  w *= 2;
  h *= 2;
#endif
  buffer.pixels.resize(w*h);
  buffer.w = w;
  buffer.h = h;
}

void Window::refresh()
{
  objc_msgSend((id)view, sel_getUid("setNeedsDisplay:"), YES);
  //objc_msgSend((id)view, sel_getUid("setNeedsDisplay:"));
}


/*
static void *refreshThread(void *arg)
{
  CanvasWindow* win = (CanvasWindow*)arg;
  win->drawn = false;
  while (true) {
    sleepMilli(win->delay);
    if (win->drawn) {
      win->refresh();
    }
  }
}
*/

CanvasWindow::CanvasWindow()
  : Window(), delay(33) // 33ms -> 30 fps
{
  // Using a thread doesn't seem to work, the objective-C calls need to be
  // dispatched to the main thread to cause a redraw. Using timer events instead.
  // startThread(refreshThread, this);
}

void CanvasWindow::onDraw(int x1, int y1, int x2, int y2)
{
  drawn = true;
  drawCanvas(buffer, x1, y1, canvas);
}

void CanvasWindow::setFrameRate(int a_fps)
{
  delay = 1000 / a_fps;
}


BOOL AppDidFinishLaunching(id self, SEL _cmd, id notification)
{
  Application* appPtr = 0;
  object_getInstanceVariable(self, "ApplicationPtr", (void**)&appPtr);
  //printf("AppDidFinishLaunching %i\n", __LINE__);
  appPtr->onStart();
  //printf("AppDidFinishLaunching %i\n", __LINE__);
  return YES;
}

void WindowOnDraw(id self, SEL _cmd, NSRect rect)
{
  Window* winPtr = 0;
  object_getInstanceVariable(self, "WindowPtr", (void**)&winPtr);
  winPtr->onDraw(int(rect.x), int(rect.y), int(rect.x + rect.width), int(rect.y + rect.height));
  winPtr->flush();
}

void WindowOnResize(id self, SEL _cmd, NSSize newSize)
{
  Window* winPtr = 0;
  object_getInstanceVariable(self, "WindowPtr", (void**)&winPtr);
  winPtr->onResize(int(newSize.width), int(newSize.height));
  winPtr->onDraw(0, 0, winPtr->getWidth(), winPtr->getHeight());
}

void WindowOnMouseEvent(id self, SEL _cmd, NSEvent* event)
{
  Window* winPtr = 0;
  object_getInstanceVariable(self, "WindowPtr", (void**)&winPtr);
  NSPoint pnt = (*(NSPoint(*)(id, SEL, ...))&objc_msgSend)((id)event, sel_getUid("locationInWindow"));
  unsigned buttons = (*(unsigned(*)(id, SEL, ...))&objc_msgSend)((id)objc_getClass("NSEvent"), sel_getUid("pressedMouseButtons"));
#if USE_RETINA
  int x = int(pnt.x);
  int y = (winPtr->getHeight()/2)-int(pnt.y);
  //x *= 2;
  //y *= 2;
#else
  int x = int(pnt.x);
  int y = winPtr->getHeight()-int(pnt.y);
#endif
  winPtr->onMouseEvent(x, y, buttons);
}

void WindowOnTimerEvent(id self, SEL _cmd, void* param)
{
  Window* winPtr = 0;
  object_getInstanceVariable(self, "WindowPtr", (void**)&winPtr);
  winPtr->onTimerEvent();
  winPtr->onDraw(0, 0, winPtr->getWidth(), winPtr->getHeight());
  winPtr->flush();
}