myou-engine/libs/glfm/src/glfm_apple.m
Alberto Torres 9af1322937 First commit.
* Incomplete port of myou-engine-js to nimskull, after many months of work, and
  a few extra features that weren't exactly necessary for a "first commit" to
  work. Excuse the lack of commit history up to this point.
* Bare bones structure of the documentation and the process to update it.
* Restructure of the whole project to have a more sensible organization.
* Making submodules of forks of larger libraries.
* README, licenses, AUTHORS.md.
2024-08-20 13:08:19 +02:00

3571 lines
130 KiB
Objective-C

// GLFM
// https://github.com/brackeen/glfm
#if defined(__APPLE__)
#include "glfm.h"
#include "glfm_internal.h"
#if !defined(GLFM_INCLUDE_METAL)
# define GLFM_INCLUDE_METAL 1
#endif
#if TARGET_OS_IOS || TARGET_OS_TV
# import <UIKit/UIKit.h>
#elif TARGET_OS_OSX
# import <AppKit/AppKit.h>
# import <Carbon/Carbon.h> // For kVK key codes in HIToolbox/Events.h
# import <IOKit/hidsystem/IOLLEvent.h> // For NX_DEVICE modifier keys
# define UIView NSView
# define UIViewAutoresizingFlexibleWidth NSViewWidthSizable
# define UIViewAutoresizingFlexibleHeight NSViewHeightSizable
# define UIViewController NSViewController
# define UIWindow NSWindow
#endif
#if TARGET_OS_IOS
# import <CoreHaptics/CoreHaptics.h>
# import <CoreMotion/CoreMotion.h>
#endif
#if GLFM_INCLUDE_METAL
# import <MetalKit/MetalKit.h>
#endif
#include <dlfcn.h>
#ifdef NDEBUG
# define GLFM_LOG(...) do { } while (0)
#else
# define GLFM_LOG(...) NSLog(@__VA_ARGS__)
#endif
#if __has_feature(objc_arc)
# define GLFM_AUTORELEASE(value) value
# define GLFM_RELEASE(value) ((void)0)
# define GLFM_WEAK __weak
#else
# define GLFM_AUTORELEASE(value) [value autorelease]
# define GLFM_RELEASE(value) [value release]
# define GLFM_WEAK __unsafe_unretained
#endif
#if TARGET_OS_IOS || TARGET_OS_TV
# define GLFM_MAX_SIMULTANEOUS_TOUCHES 10
# ifdef NDEBUG
# define GLFM_CHECK_GL_ERROR() ((void)0)
# else
# define GLFM_CHECK_GL_ERROR() do { GLenum error = glGetError(); if (error != GL_NO_ERROR) \
GLFM_LOG("OpenGL error 0x%04x at glfm_apple.m:%i", error, __LINE__); } while(0)
# endif
#endif
static bool glfm__isCGFloatEqual(CGFloat value1, CGFloat value2) {
#if CGFLOAT_IS_DOUBLE
return fabs(value1 - value2) <= DBL_EPSILON;
#else
return fabsf(value1 - value2) <= FLT_EPSILON;
#endif
}
static void glfm__getDefaultDisplaySize(const GLFMDisplay *display,
double *width, double *height, double *scale);
static void glfm__getDrawableSize(double displayWidth, double displayHeight, double displayScale,
int *width, int *height);
// MARK: - GLFMView protocol
@protocol GLFMView
@property(nonatomic, readonly) GLFMRenderingAPI renderingAPI;
@property(nonatomic, readonly) int drawableWidth;
@property(nonatomic, readonly) int drawableHeight;
@property(nonatomic, readonly) BOOL surfaceCreatedNotified;
@property(nonatomic, assign) BOOL animating;
@property(nonatomic, copy, nullable) void (^preRenderCallback)(void);
- (void)draw;
- (void)swapBuffers;
- (void)requestRefresh;
@end
// MARK: GLFMNullView
@interface GLFMNullView : UIView <GLFMView>
@end
@implementation GLFMNullView
@synthesize preRenderCallback = _preRenderCallback;
- (GLFMRenderingAPI)renderingAPI {
return GLFMRenderingAPIOpenGLES2;
}
- (int)drawableWidth {
return 0;
}
- (int)drawableHeight {
return 0;
}
- (BOOL)animating {
return NO;
}
- (BOOL)surfaceCreatedNotified {
return NO;
}
- (void)setAnimating:(BOOL)animating {
(void)animating;
}
- (void)draw {
if (_preRenderCallback) {
_preRenderCallback();
}
}
- (void)swapBuffers {
}
- (void)requestRefresh {
}
- (void)dealloc {
GLFM_RELEASE(_preRenderCallback);
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
@end
#if GLFM_INCLUDE_METAL
// MARK: - GLFMMetalView
@interface GLFMMetalView : MTKView <GLFMView, MTKViewDelegate>
@property(nonatomic, assign) GLFMDisplay *glfmDisplay;
@property(nonatomic, assign) int drawableWidth;
@property(nonatomic, assign) int drawableHeight;
@property(nonatomic, assign) BOOL surfaceCreatedNotified;
@property(nonatomic, assign) BOOL refreshRequested;
@property(nonatomic, assign) BOOL isDrawing;
@end
#if TARGET_OS_OSX
@interface GLFMMetalView () <NSTextInputClient>
@end
#endif // TARGET_OS_OSX
@implementation GLFMMetalView
@synthesize drawableWidth, drawableHeight, surfaceCreatedNotified, refreshRequested, isDrawing;
@synthesize glfmDisplay = _glfmDisplay, preRenderCallback = _preRenderCallback;
@dynamic renderingAPI, animating;
- (instancetype)initWithFrame:(CGRect)frame contentScaleFactor:(CGFloat)contentScaleFactor
device:(id<MTLDevice>)device glfmDisplay:(GLFMDisplay *)glfmDisplay {
if ((self = [super initWithFrame:frame device:device])) {
#if TARGET_OS_IOS || TARGET_OS_TV
self.contentScaleFactor = contentScaleFactor;
#else
self.layer.contentsScale = contentScaleFactor;
#endif
self.delegate = self;
self.glfmDisplay = glfmDisplay;
self.drawableWidth = (int)self.drawableSize.width;
self.drawableHeight = (int)self.drawableSize.height;
[self requestRefresh];
switch (glfmDisplay->colorFormat) {
case GLFMColorFormatRGB565:
if (@available(iOS 8, tvOS 8, macOS 11, *)) {
self.colorPixelFormat = MTLPixelFormatB5G6R5Unorm;
} else {
self.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
}
break;
case GLFMColorFormatRGBA8888:
default:
self.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
break;
}
if (glfmDisplay->depthFormat == GLFMDepthFormatNone &&
glfmDisplay->stencilFormat == GLFMStencilFormatNone) {
self.depthStencilPixelFormat = MTLPixelFormatInvalid;
} else if (glfmDisplay->depthFormat == GLFMDepthFormatNone) {
self.depthStencilPixelFormat = MTLPixelFormatStencil8;
} else if (glfmDisplay->stencilFormat == GLFMStencilFormatNone) {
if (@available(iOS 13, tvOS 13, macOS 10.12, *)) {
if (glfmDisplay->depthFormat == GLFMDepthFormat16) {
self.depthStencilPixelFormat = MTLPixelFormatDepth16Unorm;
} else {
self.depthStencilPixelFormat = MTLPixelFormatDepth32Float;
}
} else {
self.depthStencilPixelFormat = MTLPixelFormatDepth32Float;
}
} else {
self.depthStencilPixelFormat = MTLPixelFormatDepth32Float_Stencil8;
}
self.sampleCount = (glfmDisplay->multisample == GLFMMultisampleNone) ? 1 : 4;
}
return self;
}
- (GLFMRenderingAPI)renderingAPI {
return GLFMRenderingAPIMetal;
}
#if TARGET_OS_OSX
- (void)viewDidChangeBackingProperties {
[super viewDidChangeBackingProperties];
if (self.window) {
self.layer.contentsScale = self.window.backingScaleFactor;
}
}
- (void)setFrameSize:(NSSize)newSize {
// For live resizing
[super setFrameSize:newSize];
if (self.surfaceCreatedNotified) {
[self requestRefresh];
if (self.isLiveResizing) {
[self draw];
}
}
}
- (void)viewWillStartLiveResize {
self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft;
[super viewWillStartLiveResize];
}
- (void)viewDidEndLiveResize {
self.layerContentsPlacement = NSViewLayerContentsPlacementScaleAxesIndependently;
[super viewDidEndLiveResize];
}
- (BOOL)isLiveResizing {
return self.layerContentsPlacement == NSViewLayerContentsPlacementTopLeft;
}
- (BOOL)acceptsFirstResponder {
return YES;
}
#endif // TARGET_OS_OSX
- (void)draw {
if (!self.isDrawing) {
[super draw];
}
}
- (BOOL)animating {
return !self.paused;
}
- (void)setAnimating:(BOOL)animating {
if (self.animating != animating) {
self.paused = !animating;
[self requestRefresh];
}
}
- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size {
}
- (void)drawInMTKView:(MTKView *)view {
if (self.isDrawing) {
return;
}
self.isDrawing = YES;
int newDrawableWidth = (int)self.drawableSize.width;
int newDrawableHeight = (int)self.drawableSize.height;
if (!self.surfaceCreatedNotified) {
self.surfaceCreatedNotified = YES;
[self requestRefresh];
self.drawableWidth = newDrawableWidth;
self.drawableHeight = newDrawableHeight;
if (self.glfmDisplay->surfaceCreatedFunc) {
self.glfmDisplay->surfaceCreatedFunc(self.glfmDisplay,
self.drawableWidth, self.drawableHeight);
}
} else if (self.drawableWidth != newDrawableWidth || self.drawableHeight != newDrawableHeight) {
[self requestRefresh];
self.drawableWidth = newDrawableWidth;
self.drawableHeight = newDrawableHeight;
if (self.glfmDisplay->surfaceResizedFunc) {
self.glfmDisplay->surfaceResizedFunc(self.glfmDisplay,
self.drawableWidth, self.drawableHeight);
}
}
if (_preRenderCallback) {
_preRenderCallback();
}
if (self.refreshRequested) {
self.refreshRequested = NO;
if (self.glfmDisplay->surfaceRefreshFunc) {
self.glfmDisplay->surfaceRefreshFunc(self.glfmDisplay);
}
}
if (self.glfmDisplay->renderFunc) {
self.glfmDisplay->renderFunc(self.glfmDisplay);
}
self.isDrawing = NO;
}
- (void)swapBuffers {
// Do nothing
}
- (void)requestRefresh {
self.refreshRequested = YES;
}
#if TARGET_OS_IOS || TARGET_OS_TV
- (void)layoutSubviews {
// First render as soon as safeAreaInsets are set
if (!self.surfaceCreatedNotified) {
[self requestRefresh];
[self draw];
}
}
#endif
- (void)dealloc {
GLFM_RELEASE(_preRenderCallback);
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
#if TARGET_OS_OSX
// MARK: NSTextInputClient
// NOTE: This code is the same in GLFMOpenGLView
- (void)insertText:(id)text replacementRange:(NSRange)replacementRange {
// Input from the Character Palette
if (self.glfmDisplay->charFunc) {
NSString *string;
if ([(NSObject *)text isKindOfClass:[NSAttributedString class]]) {
string = ((NSAttributedString *)text).string;
} else {
string = text;
}
self.glfmDisplay->charFunc(self.glfmDisplay, string.UTF8String, 0);
}
}
- (void)doCommandBySelector:(SEL)selector {
}
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange {
}
- (void)unmarkText {
}
- (NSRange)selectedRange {
return NSMakeRange(0, 0);
}
- (NSRange)markedRange {
return NSMakeRange(NSNotFound, 0);
}
- (BOOL)hasMarkedText {
return NO;
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
return nil;
}
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
return @[];
}
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
// This is called for positioning the Character Palette, but seems to be ignored.
NSRect frame = self.window.frame;
return CGRectMake(NSMidX(frame), NSMidY(frame), 0, 0);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point {
return 0;
}
#endif // TARGET_OS_OSX
@end
#endif // GLFM_INCLUDE_METAL
#if TARGET_OS_IOS || TARGET_OS_TV
// MARK: - GLFMOpenGLESView (iOS, tvOS)
@interface GLFMOpenGLESView : UIView <GLFMView>
@property(nonatomic, assign) GLFMDisplay *glfmDisplay;
@property(nonatomic, assign) GLFMRenderingAPI renderingAPI;
@property(nonatomic, strong) CADisplayLink *displayLink;
@property(nonatomic, strong) EAGLContext *context;
@property(nonatomic, strong) NSString *colorFormat;
@property(nonatomic, assign) BOOL preserveBackbuffer;
@property(nonatomic, assign) NSUInteger depthBits;
@property(nonatomic, assign) NSUInteger stencilBits;
@property(nonatomic, assign) BOOL multisampling;
@property(nonatomic, assign) BOOL surfaceCreatedNotified;
@property(nonatomic, assign) BOOL surfaceSizeChanged;
@property(nonatomic, assign) BOOL refreshRequested;
@property(nonatomic, assign) BOOL isDrawing;
@end
@implementation GLFMOpenGLESView {
GLint _drawableWidth;
GLint _drawableHeight;
GLuint _defaultFramebuffer;
GLuint _colorRenderbuffer;
GLuint _attachmentRenderbuffer;
GLuint _msaaFramebuffer;
GLuint _msaaRenderbuffer;
}
@synthesize renderingAPI, displayLink, context, colorFormat, preserveBackbuffer;
@synthesize depthBits, stencilBits, multisampling;
@synthesize surfaceCreatedNotified, surfaceSizeChanged, refreshRequested, isDrawing;
@synthesize glfmDisplay = _glfmDisplay, preRenderCallback = _preRenderCallback;
@dynamic drawableWidth, drawableHeight, animating;
+ (Class)layerClass {
return [CAEAGLLayer class];
}
- (instancetype)initWithFrame:(CGRect)frame contentScaleFactor:(CGFloat)contentScaleFactor
glfmDisplay:(GLFMDisplay *)glfmDisplay {
if ((self = [super initWithFrame:frame])) {
self.contentScaleFactor = contentScaleFactor;
self.glfmDisplay = glfmDisplay;
[self requestRefresh];
if (glfmDisplay->preferredAPI >= GLFMRenderingAPIOpenGLES3) {
self.context = GLFM_AUTORELEASE([[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]);
self.renderingAPI = GLFMRenderingAPIOpenGLES3;
}
if (!self.context) {
self.context = GLFM_AUTORELEASE([[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]);
self.renderingAPI = GLFMRenderingAPIOpenGLES2;
}
if (!self.context) {
GLFM_LOG("Failed to create ES context");
glfm__reportSurfaceError(glfmDisplay, "Failed to create ES context");
GLFM_RELEASE(self);
return nil;
}
switch (glfmDisplay->colorFormat) {
case GLFMColorFormatRGB565:
self.colorFormat = kEAGLColorFormatRGB565;
break;
case GLFMColorFormatRGBA8888:
default:
self.colorFormat = kEAGLColorFormatRGBA8;
break;
}
switch (glfmDisplay->depthFormat) {
case GLFMDepthFormatNone:
default:
self.depthBits = 0;
break;
case GLFMDepthFormat16:
self.depthBits = 16;
break;
case GLFMDepthFormat24:
self.depthBits = 24;
break;
}
switch (glfmDisplay->stencilFormat) {
case GLFMStencilFormatNone:
default:
self.stencilBits = 0;
break;
case GLFMStencilFormat8:
self.stencilBits = 8;
break;
}
self.multisampling = glfmDisplay->multisample != GLFMMultisampleNone;
[self createDrawable];
}
return self;
}
- (void)dealloc {
self.animating = NO;
[self deleteDrawable];
if ([EAGLContext currentContext] == self.context) {
[EAGLContext setCurrentContext:nil];
}
self.context = nil;
self.colorFormat = nil;
GLFM_RELEASE(_preRenderCallback);
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
- (int)drawableWidth {
return (int)_drawableWidth;
}
- (int)drawableHeight {
return (int)_drawableHeight;
}
- (BOOL)animating {
return (self.displayLink != nil);
}
- (void)setAnimating:(BOOL)animating {
if (self.animating != animating) {
[self requestRefresh];
if (!animating) {
[self.displayLink invalidate];
self.displayLink = nil;
} else {
self.displayLink = [CADisplayLink displayLinkWithTarget:self
selector:@selector(render:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
}
}
- (void)createDrawable {
if (_defaultFramebuffer != 0 || !self.context) {
return;
}
if (!self.colorFormat) {
self.colorFormat = kEAGLColorFormatRGBA8;
}
[EAGLContext setCurrentContext:self.context];
CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
eaglLayer.opaque = YES;
eaglLayer.drawableProperties =
@{ kEAGLDrawablePropertyRetainedBacking : @(self.preserveBackbuffer),
kEAGLDrawablePropertyColorFormat : self.colorFormat };
glGenFramebuffers(1, &_defaultFramebuffer);
glGenRenderbuffers(1, &_colorRenderbuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _defaultFramebuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
// iPhone 6 Display Zoom hack - use a modified bounds so that the renderbufferStorage method
// creates the correct size renderbuffer.
CGRect oldBounds = eaglLayer.bounds;
if (glfm__isCGFloatEqual(eaglLayer.contentsScale, (CGFloat)2.343750)) {
if (glfm__isCGFloatEqual(eaglLayer.bounds.size.width, (CGFloat)320.0) &&
glfm__isCGFloatEqual(eaglLayer.bounds.size.height, (CGFloat)568.0)) {
eaglLayer.bounds = CGRectMake(eaglLayer.bounds.origin.x, eaglLayer.bounds.origin.y,
eaglLayer.bounds.size.width,
1334 / eaglLayer.contentsScale);
} else if (glfm__isCGFloatEqual(eaglLayer.bounds.size.width, (CGFloat)568.0) &&
glfm__isCGFloatEqual(eaglLayer.bounds.size.height, (CGFloat)320.0)) {
eaglLayer.bounds = CGRectMake(eaglLayer.bounds.origin.x, eaglLayer.bounds.origin.y,
1334 / eaglLayer.contentsScale,
eaglLayer.bounds.size.height);
}
}
if (![self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:eaglLayer]) {
GLFM_LOG("Error: Call to renderbufferStorage failed");
}
eaglLayer.bounds = oldBounds;
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
_colorRenderbuffer);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_drawableWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_drawableHeight);
if (self.multisampling) {
glGenFramebuffers(1, &_msaaFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _msaaFramebuffer);
glGenRenderbuffers(1, &_msaaRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _msaaRenderbuffer);
GLenum internalFormat = GL_RGBA8_OES;
if ([kEAGLColorFormatRGB565 isEqualToString:self.colorFormat]) {
internalFormat = GL_RGB565;
}
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, 4, internalFormat,
_drawableWidth, _drawableHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
_msaaRenderbuffer);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
GLFM_LOG("Error: Couldn't create multisample framebuffer: 0x%04x", status);
}
}
if (self.depthBits > 0 || self.stencilBits > 0) {
glGenRenderbuffers(1, &_attachmentRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _attachmentRenderbuffer);
GLenum internalFormat;
if (self.depthBits > 0 && self.stencilBits > 0) {
internalFormat = GL_DEPTH24_STENCIL8_OES;
} else if (self.depthBits >= 24) {
internalFormat = GL_DEPTH_COMPONENT24_OES;
} else if (self.depthBits > 0) {
internalFormat = GL_DEPTH_COMPONENT16;
} else {
internalFormat = GL_STENCIL_INDEX8;
}
if (self.multisampling) {
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, 4, internalFormat,
_drawableWidth, _drawableHeight);
} else {
glRenderbufferStorage(GL_RENDERBUFFER, internalFormat, _drawableWidth, _drawableHeight);
}
if (self.depthBits > 0) {
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
_attachmentRenderbuffer);
}
if (self.stencilBits > 0) {
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER,
_attachmentRenderbuffer);
}
}
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
GLFM_LOG("Error: Framebuffer incomplete: 0x%04x", status);
}
GLFM_CHECK_GL_ERROR();
}
- (void)deleteDrawable {
[EAGLContext setCurrentContext:self.context];
if (_defaultFramebuffer) {
glDeleteFramebuffers(1, &_defaultFramebuffer);
_defaultFramebuffer = 0;
}
if (_colorRenderbuffer) {
glDeleteRenderbuffers(1, &_colorRenderbuffer);
_colorRenderbuffer = 0;
}
if (_attachmentRenderbuffer) {
glDeleteRenderbuffers(1, &_attachmentRenderbuffer);
_attachmentRenderbuffer = 0;
}
if (_msaaRenderbuffer) {
glDeleteRenderbuffers(1, &_msaaRenderbuffer);
_msaaRenderbuffer = 0;
}
if (_msaaFramebuffer) {
glDeleteFramebuffers(1, &_msaaFramebuffer);
_msaaFramebuffer = 0;
}
}
- (void)prepareRender {
[EAGLContext setCurrentContext:self.context];
if (self.multisampling) {
glBindFramebuffer(GL_FRAMEBUFFER, _msaaFramebuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _msaaRenderbuffer);
} else {
glBindFramebuffer(GL_FRAMEBUFFER, _defaultFramebuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
}
GLFM_CHECK_GL_ERROR();
}
- (void)swapBuffers {
[EAGLContext setCurrentContext:self.context];
if (self.multisampling) {
glBindFramebuffer(GL_READ_FRAMEBUFFER_APPLE, _msaaFramebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER_APPLE, _defaultFramebuffer);
glResolveMultisampleFramebufferAPPLE();
}
static bool checked_GL_EXT_discard_framebuffer = false;
static bool has_GL_EXT_discard_framebuffer = false;
if (!checked_GL_EXT_discard_framebuffer) {
checked_GL_EXT_discard_framebuffer = true;
const char *extensions = (const char *)glGetString(GL_EXTENSIONS);
if (extensions) {
has_GL_EXT_discard_framebuffer = strstr(extensions, "GL_EXT_discard_framebuffer") != NULL;
}
}
if (has_GL_EXT_discard_framebuffer) {
GLenum target = GL_FRAMEBUFFER;
GLenum attachments[3];
GLsizei numAttachments = 0;
if (self.multisampling) {
target = GL_READ_FRAMEBUFFER_APPLE;
attachments[numAttachments++] = GL_COLOR_ATTACHMENT0;
}
if (self.depthBits > 0) {
attachments[numAttachments++] = GL_DEPTH_ATTACHMENT;
}
if (self.stencilBits > 0) {
attachments[numAttachments++] = GL_STENCIL_ATTACHMENT;
}
if (numAttachments > 0) {
if (self.multisampling) {
glBindFramebuffer(GL_FRAMEBUFFER, _msaaFramebuffer);
} else {
glBindFramebuffer(GL_FRAMEBUFFER, _defaultFramebuffer);
}
glDiscardFramebufferEXT(target, numAttachments, attachments);
}
}
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
[self.context presentRenderbuffer:GL_RENDERBUFFER];
GLFM_CHECK_GL_ERROR();
}
- (void)render:(CADisplayLink *)displayLink {
if (self.isDrawing) {
return;
}
self.isDrawing = YES;
[EAGLContext setCurrentContext:self.context];
if (!self.surfaceCreatedNotified) {
self.surfaceCreatedNotified = YES;
[self requestRefresh];
if (self.glfmDisplay->surfaceCreatedFunc) {
self.glfmDisplay->surfaceCreatedFunc(self.glfmDisplay,
self.drawableWidth, self.drawableHeight);
}
}
if (self.surfaceSizeChanged) {
self.surfaceSizeChanged = NO;
[self requestRefresh];
if (self.glfmDisplay->surfaceResizedFunc) {
self.glfmDisplay->surfaceResizedFunc(self.glfmDisplay,
self.drawableWidth, self.drawableHeight);
}
}
if (_preRenderCallback) {
_preRenderCallback();
}
if (self.refreshRequested) {
self.refreshRequested = NO;
if (self.glfmDisplay->surfaceRefreshFunc) {
self.glfmDisplay->surfaceRefreshFunc(self.glfmDisplay);
}
}
if (self.glfmDisplay->renderFunc) {
[self prepareRender];
self.glfmDisplay->renderFunc(self.glfmDisplay);
}
self.isDrawing = NO;
}
- (void)draw {
if (self.displayLink && !self.isDrawing) {
[self render:self.displayLink];
}
}
- (void)requestRefresh {
self.refreshRequested = YES;
}
- (void)layoutSubviews {
int newDrawableWidth;
int newDrawableHeight;
glfm__getDrawableSize((double)self.bounds.size.width, (double)self.bounds.size.height,
(double)self.contentScaleFactor, &newDrawableWidth, &newDrawableHeight);
if (self.drawableWidth != newDrawableWidth || self.drawableHeight != newDrawableHeight) {
[self deleteDrawable];
[self createDrawable];
self.surfaceSizeChanged = self.surfaceCreatedNotified;
}
// First render as soon as safeAreaInsets are set
if (!self.surfaceCreatedNotified) {
[self requestRefresh];
[self draw];
}
}
@end
#endif // TARGET_OS_IOS || TARGET_OS_TV
#if TARGET_OS_OSX
// MARK: - GLFMOpenGLView (macOS)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@interface GLFMOpenGLView : NSOpenGLView <GLFMView, NSTextInputClient>
@property(nonatomic, assign) GLFMDisplay *glfmDisplay;
@property(nonatomic, assign) int drawableWidth;
@property(nonatomic, assign) int drawableHeight;
@property(nonatomic, assign) BOOL surfaceCreatedNotified;
@property(nonatomic, assign) BOOL refreshRequested;
@property(nonatomic, assign) BOOL isDrawing;
@end
@implementation GLFMOpenGLView {
CVDisplayLinkRef _displayLink;
dispatch_source_t _displaySource;
}
@synthesize glfmDisplay = _glfmDisplay, preRenderCallback = _preRenderCallback;
@synthesize drawableWidth, drawableHeight;
@synthesize surfaceCreatedNotified, refreshRequested, isDrawing;
@dynamic renderingAPI, animating;
- (instancetype)initWithFrame:(CGRect)frame
glfmDisplay:(GLFMDisplay *)glfmDisplay {
uint32_t colorBits = (glfmDisplay->colorFormat == GLFMColorFormatRGB565) ? 16 : 32;
uint32_t alphaBits = (glfmDisplay->colorFormat == GLFMColorFormatRGB565) ? 0 : 8;
uint32_t stencilBits = (glfmDisplay->stencilFormat == GLFMStencilFormat8) ? 8 : 0;
uint32_t sampleCount = (glfmDisplay->multisample == GLFMMultisample4X) ? 4 : 1;
uint32_t depthBits;
switch (glfmDisplay->depthFormat) {
case GLFMDepthFormatNone:
default:
depthBits = 0;
break;
case GLFMDepthFormat16:
depthBits = 16;
break;
case GLFMDepthFormat24:
depthBits = 24;
break;
}
// Set up attributes and create pixel format
NSOpenGLPixelFormatAttribute attributes[32];
size_t index = 0;
attributes[index++] = kCGLPFASupportsAutomaticGraphicsSwitching;
attributes[index++] = NSOpenGLPFAAccelerated;
attributes[index++] = NSOpenGLPFAAllowOfflineRenderers;
attributes[index++] = NSOpenGLPFAClosestPolicy;
attributes[index++] = NSOpenGLPFADoubleBuffer;
if (glfmDisplay->swapBehavior == GLFMSwapBehaviorBufferPreserved) {
attributes[index++] = NSOpenGLPFABackingStore;
} else if (glfmDisplay->swapBehavior == GLFMSwapBehaviorBufferDestroyed) {
attributes[index++] = kCGLPFABackingVolatile;
}
attributes[index++] = NSOpenGLPFAOpenGLProfile;
attributes[index++] = NSOpenGLProfileVersion3_2Core;
attributes[index++] = NSOpenGLPFAColorSize;
attributes[index++] = colorBits;
if (alphaBits > 0) {
attributes[index++] = NSOpenGLPFAAlphaSize;
attributes[index++] = alphaBits;
}
if (depthBits > 0) {
attributes[index++] = NSOpenGLPFADepthSize;
attributes[index++] = depthBits;
}
if (stencilBits > 0) {
attributes[index++] = NSOpenGLPFAStencilSize;
attributes[index++] = stencilBits;
}
if (sampleCount > 1) {
attributes[index++] = NSOpenGLPFASampleBuffers;
attributes[index++] = 1;
attributes[index++] = NSOpenGLPFASamples;
attributes[index++] = sampleCount;
}
attributes[index] = 0;
assert(index < sizeof(attributes) / sizeof(attributes[0]));
NSOpenGLPixelFormat *pixelFormat = GLFM_AUTORELEASE([[NSOpenGLPixelFormat alloc]
initWithAttributes:attributes]);
if (!pixelFormat) {
GLFM_LOG("Failed to create GL pixel format");
glfm__reportSurfaceError(glfmDisplay, "Failed to create GL pixel format");
return nil;
}
// Initialize view
self = [super initWithFrame:frame pixelFormat:pixelFormat];
NSOpenGLContext *openGLContext = self.openGLContext;
CGLContextObj cglContext = openGLContext.CGLContextObj;
CGLPixelFormatObj cglPixelFormat = self.pixelFormat.CGLPixelFormatObj;
if (!openGLContext || !cglContext || !cglPixelFormat) {
GLFM_LOG("Failed to create GL context");
glfm__reportSurfaceError(glfmDisplay, "Failed to create GL context");
GLFM_RELEASE(self);
return nil;
}
GLint swapInterval = 1;
[openGLContext setValues:&swapInterval forParameter:NSOpenGLContextParameterSwapInterval];
self.wantsBestResolutionOpenGLSurface = YES;
self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft;
self.glfmDisplay = glfmDisplay;
#if 0
// Print attributes
{
#define printAttribute(attr) do { \
GLint virtualScreen = [self.openGLContext currentVirtualScreen]; \
[pixelFormat getValues:&value forAttribute:(attr) forVirtualScreen:virtualScreen]; \
printf(" " #attr ": %i\n", value); \
} while (0)
GLint value;
printAttribute(NSOpenGLPFAColorSize);
printAttribute(NSOpenGLPFAAlphaSize);
printAttribute(NSOpenGLPFADepthSize);
printAttribute(NSOpenGLPFAStencilSize);
printAttribute(NSOpenGLPFASampleBuffers);
printAttribute(NSOpenGLPFASamples);
#undef printAttribute
}
#endif
// Initialize display link
GLFM_WEAK __typeof(self) weakSelf = self;
_displaySource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,
dispatch_get_main_queue());
dispatch_source_set_event_handler(_displaySource, ^{
[weakSelf draw];
});
dispatch_resume(_displaySource);
CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
CVDisplayLinkSetOutputHandler(_displayLink, ^CVReturn(CVDisplayLinkRef displayLink,
const CVTimeStamp *now,
const CVTimeStamp *outputTime,
CVOptionFlags flags,
CVOptionFlags *flagsOut) {
(void)displayLink;
(void)now;
(void)outputTime;
(void)flags;
(void)flagsOut;
__typeof(self) strongSelf = weakSelf;
if (strongSelf) {
dispatch_source_merge_data(strongSelf->_displaySource, 1);
}
return kCVReturnSuccess;
});
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
return self;
}
- (BOOL)acceptsFirstResponder {
return YES;
}
- (void)viewDidChangeBackingProperties {
[self requestRefresh];
[self update];
}
- (GLFMRenderingAPI)renderingAPI {
// TODO: Return correct API
return GLFMRenderingAPIOpenGLES2;
}
- (BOOL)animating {
return CVDisplayLinkIsRunning(_displayLink) == TRUE;
}
- (void)setAnimating:(BOOL)animating {
if (self.animating != animating) {
if (animating) {
CVDisplayLinkStart(_displayLink);
} else {
CVDisplayLinkStop(_displayLink);
}
[self requestRefresh];
}
}
- (void)drawRect:(NSRect)dirtyRect {
// For live resizing
NSRect viewRectPixels = [self convertRectToBacking:self.bounds];
int newDrawableWidth = (int)viewRectPixels.size.width;
int newDrawableHeight = (int)viewRectPixels.size.height;
if (self.surfaceCreatedNotified &&
(self.drawableWidth != newDrawableWidth || self.drawableHeight != newDrawableHeight)) {
[self requestRefresh];
if (self.animating) {
CVDisplayLinkStop(_displayLink);
[self draw];
CVDisplayLinkStart(_displayLink);
} else {
[self draw];
}
}
}
- (void)draw {
if (self.isDrawing) {
return;
}
self.isDrawing = YES;
NSRect viewRectPixels = [self convertRectToBacking:self.bounds];
int newDrawableWidth = (int)viewRectPixels.size.width;
int newDrawableHeight = (int)viewRectPixels.size.height;
assert([NSThread isMainThread]);
[self.openGLContext makeCurrentContext];
if (!self.surfaceCreatedNotified) {
self.surfaceCreatedNotified = YES;
[self requestRefresh];
self.drawableWidth = newDrawableWidth;
self.drawableHeight = newDrawableHeight;
if (self.glfmDisplay->surfaceCreatedFunc) {
self.glfmDisplay->surfaceCreatedFunc(self.glfmDisplay,
self.drawableWidth, self.drawableHeight);
}
} else if (self.drawableWidth != newDrawableWidth || self.drawableHeight != newDrawableHeight) {
self.drawableWidth = newDrawableWidth;
self.drawableHeight = newDrawableHeight;
[self requestRefresh];
if (self.glfmDisplay->surfaceResizedFunc) {
self.glfmDisplay->surfaceResizedFunc(self.glfmDisplay,
self.drawableWidth, self.drawableHeight);
}
}
if (_preRenderCallback) {
_preRenderCallback();
}
if (self.refreshRequested) {
self.refreshRequested = NO;
if (self.glfmDisplay->surfaceRefreshFunc) {
self.glfmDisplay->surfaceRefreshFunc(self.glfmDisplay);
}
}
if (self.glfmDisplay->renderFunc) {
self.glfmDisplay->renderFunc(self.glfmDisplay);
}
self.isDrawing = NO;
}
- (void)swapBuffers {
[self.openGLContext flushBuffer];
}
- (void)requestRefresh {
self.refreshRequested = YES;
}
- (void)dealloc {
self.animating = NO;
if ([NSOpenGLContext currentContext] == self.openGLContext) {
[NSOpenGLContext clearCurrentContext];
}
dispatch_source_cancel(_displaySource);
CVDisplayLinkRelease(_displayLink);
GLFM_RELEASE(_preRenderCallback);
#if !__has_feature(objc_arc)
dispatch_release(_displaySource);
[super dealloc];
#endif
}
// MARK: NSTextInputClient
// NOTE: This code is the same in GLFMMetalView
- (void)insertText:(id)text replacementRange:(NSRange)replacementRange {
// Input from the Character Palette
if (self.glfmDisplay->charFunc) {
NSString *string;
if ([(NSObject *)text isKindOfClass:[NSAttributedString class]]) {
string = ((NSAttributedString *)text).string;
} else {
string = text;
}
self.glfmDisplay->charFunc(self.glfmDisplay, string.UTF8String, 0);
}
}
- (void)doCommandBySelector:(SEL)selector {
}
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange {
}
- (void)unmarkText {
}
- (NSRange)selectedRange {
return NSMakeRange(0, 0);
}
- (NSRange)markedRange {
return NSMakeRange(NSNotFound, 0);
}
- (BOOL)hasMarkedText {
return NO;
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
return nil;
}
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
return @[];
}
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange {
// This is called for positioning the Character Palette, but seems to be ignored.
NSRect frame = self.window.frame;
return CGRectMake(NSMidX(frame), NSMidY(frame), 0, 0);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point {
return 0;
}
@end
#pragma clang diagnostic pop // "-Wdeprecated-declarations"
#endif // TARGET_OS_OSX
// MARK: - GLFMWindow interface
@interface GLFMWindow : UIWindow
@property(nonatomic, assign) BOOL active;
@end
// MARK: - GLFMViewController
@interface GLFMViewController : UIViewController
@property(nonatomic, assign) GLFMDisplay *glfmDisplay;
@property(nonatomic, assign) CGRect defaultFrame;
@property(nonatomic, assign) CGFloat defaultContentScale;
#if GLFM_INCLUDE_METAL
@property(nonatomic, strong) id<MTLDevice> metalDevice;
#endif
@end
#if TARGET_OS_IOS
@interface GLFMViewController () <UIKeyInput, UITextInputTraits, UIPointerInteractionDelegate>
@property(nonatomic, assign) BOOL keyboardRequested;
@property(nonatomic, strong) UIView *noSoftKeyboardView;
@property(nonatomic, strong) CMMotionManager *motionManager;
@property(nonatomic, assign) UIInterfaceOrientation orientation;
@property(nonatomic, assign) BOOL multipleTouchEnabled;
@property(nonatomic, assign) GLFMMouseCursor mouseCursor;
@end
#endif // TARGET_OS_IOS
#if TARGET_OS_OSX
@interface GLFMViewController ()
@property(nonatomic, assign) NSEdgeInsets insets;
@property(nonatomic, strong) NSCursor *transparentCursor;
@property(nonatomic, strong) NSCursor *currentCursor;
@property(nonatomic, assign) BOOL hideMouseCursorWhileTyping;
@property(nonatomic, assign) BOOL mouseInside;
@property(nonatomic, assign) BOOL fnModifier;
@property(nonatomic, assign) UInt32 deadKeyState;
@property(nonatomic, assign) TISInputSourceRef currentKeyboard;
@property(nonatomic, assign) const UCKeyboardLayout *keyboardLayout;
@property(nonatomic, unsafe_unretained) id keyEventMonitor;
@property(nonatomic, strong) NSTextInputContext *textInputContext;
@end
#endif // TARGET_OS_OSX
@implementation GLFMViewController {
#if TARGET_OS_IOS || TARGET_OS_TV
const void *activeTouches[GLFM_MAX_SIMULTANEOUS_TOUCHES];
#endif
}
@synthesize glfmDisplay, defaultFrame, defaultContentScale;
#if GLFM_INCLUDE_METAL
@synthesize metalDevice = _metalDevice;
#endif
#if TARGET_OS_IOS
@synthesize keyboardRequested, noSoftKeyboardView, motionManager = _motionManager, orientation;
@synthesize multipleTouchEnabled, mouseCursor;
#endif
#if TARGET_OS_OSX
@synthesize insets;
@synthesize transparentCursor, currentCursor, hideMouseCursorWhileTyping, mouseInside;
@synthesize fnModifier, deadKeyState = _deadKeyState, currentKeyboard, keyboardLayout, keyEventMonitor, textInputContext;
#endif
- (id)initWithDefaultFrame:(CGRect)frame contentScale:(CGFloat)contentScale {
if ((self = [super init])) {
self.glfmDisplay = calloc(1, sizeof(GLFMDisplay));
self.glfmDisplay->platformData = (__bridge void *)self;
self.glfmDisplay->supportedOrientations = GLFMInterfaceOrientationAll;
self.defaultFrame = frame;
self.defaultContentScale = contentScale;
#if TARGET_OS_IOS
self.noSoftKeyboardView = GLFM_AUTORELEASE([UIView new]);
self.mouseCursor = GLFMMouseCursorDefault;
#endif
#if TARGET_OS_OSX
// Use a transparent image for the hidden cursor
NSImage *transparentImage = [NSImage imageWithSize:NSMakeSize(16, 16) flipped:NO
drawingHandler:^BOOL(NSRect dstRect) {
NSRectFillUsingOperation(dstRect, NSCompositingOperationClear);
return YES;
}];
self.transparentCursor = GLFM_AUTORELEASE([[NSCursor alloc] initWithImage:transparentImage
hotSpot:NSZeroPoint]);
self.currentCursor = NSCursor.arrowCursor;
self.hideMouseCursorWhileTyping = YES;
// Keyboard
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardChanged:) name:NSTextInputContextKeyboardSelectionDidChangeNotification object:nil];
[self setCurrentKeyboard];
// Send key release events when the command key is down
GLFM_WEAK __typeof(self) weakSelf = self;
self.keyEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp handler:^NSEvent *(NSEvent *event) {
if (event.modifierFlags & NSEventModifierFlagCommand) {
[weakSelf.glfmViewIfLoaded.window sendEvent:event];
}
return event;
}];
#else
[self clearTouches];
#endif
}
return self;
}
- (void)dealloc {
if (self.glfmViewIfLoaded.surfaceCreatedNotified && self.glfmDisplay->surfaceDestroyedFunc) {
self.glfmDisplay->surfaceDestroyedFunc(self.glfmDisplay);
}
free(self.glfmDisplay);
self.glfmViewIfLoaded.preRenderCallback = nil;
#if TARGET_OS_IOS
self.motionManager = nil;
self.noSoftKeyboardView = nil;
#endif
#if TARGET_OS_OSX
[[NSNotificationCenter defaultCenter] removeObserver:self];
self.currentCursor = nil;
self.transparentCursor = nil;
if (self.currentKeyboard) {
CFRelease(self.currentKeyboard);
self.currentKeyboard = NULL;
}
[NSEvent removeMonitor:self.keyEventMonitor];
self.keyEventMonitor = nil;
[self.textInputContext deactivate];
self.textInputContext = nil;
#endif
#if GLFM_INCLUDE_METAL
self.metalDevice = nil;
#endif
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
#if GLFM_INCLUDE_METAL
- (id<MTLDevice>)metalDevice {
if (!_metalDevice) {
self.metalDevice = GLFM_AUTORELEASE(MTLCreateSystemDefaultDevice());
}
return _metalDevice;
}
#endif
- (UIView<GLFMView> *)glfmView {
return (UIView<GLFMView> *)self.view;
}
- (UIView<GLFMView> *)glfmViewIfLoaded {
if (self.isViewLoaded) {
return (UIView<GLFMView> *)self.view;
}
return nil;
}
- (void)loadView {
glfmMain(self.glfmDisplay);
UIView<GLFMView> *glfmView = nil;
#if GLFM_INCLUDE_METAL
if (self.glfmDisplay->preferredAPI == GLFMRenderingAPIMetal && self.metalDevice) {
glfmView = GLFM_AUTORELEASE([[GLFMMetalView alloc] initWithFrame:self.defaultFrame
contentScaleFactor:self.defaultContentScale
device:self.metalDevice
glfmDisplay:self.glfmDisplay]);
}
#endif
#if TARGET_OS_IOS || TARGET_OS_TV
if (!glfmView) {
glfmView = GLFM_AUTORELEASE([[GLFMOpenGLESView alloc] initWithFrame:self.defaultFrame
contentScaleFactor:self.defaultContentScale
glfmDisplay:self.glfmDisplay]);
}
#endif
#if TARGET_OS_OSX
if (!glfmView) {
glfmView = GLFM_AUTORELEASE([[GLFMOpenGLView alloc] initWithFrame:self.defaultFrame
glfmDisplay:self.glfmDisplay]);
}
#endif
if (!glfmView) {
assert(glfmView != nil);
glfmView = GLFM_AUTORELEASE([[GLFMNullView alloc] initWithFrame:self.defaultFrame]);
}
GLFM_WEAK __typeof(self) weakSelf = self;
glfmView.preRenderCallback = ^{
[weakSelf preRenderCallback];
};
self.view = glfmView;
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
#if TARGET_OS_OSX
// Get mouse-move events and cursor events
NSTrackingAreaOptions trackingOptions = (NSTrackingMouseMoved |
NSTrackingMouseEnteredAndExited |
NSTrackingCursorUpdate |
NSTrackingInVisibleRect |
NSTrackingActiveInKeyWindow);
[self.view addTrackingArea:GLFM_AUTORELEASE([[NSTrackingArea alloc] initWithRect:NSZeroRect
options:trackingOptions
owner:self
userInfo:nil])];
// Capture events from the Character Palette
if ([glfmView conformsToProtocol:@protocol(NSTextInputClient)]) {
self.textInputContext = GLFM_AUTORELEASE([[NSTextInputContext alloc] initWithClient:(id<NSTextInputClient>)glfmView]);
[self.textInputContext activate];
}
#endif
}
- (void)preRenderCallback {
#if TARGET_OS_IOS
[self handleMotionEvents];
#endif
}
- (void)viewDidLoad {
[super viewDidLoad];
#if TARGET_OS_IOS
self.view.multipleTouchEnabled = self.multipleTouchEnabled;
self.orientation = [[UIApplication sharedApplication] statusBarOrientation];
[self setNeedsStatusBarAppearanceUpdate];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardFrameWillChange:)
name:UIKeyboardWillChangeFrameNotification
object:self.view.window];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(deviceOrientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:self.view.window];
if (@available(iOS 13.4, *)) {
UIHoverGestureRecognizer *hover = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hover:)];
[self.view addGestureRecognizer:hover];
GLFM_RELEASE(hover);
UIPointerInteraction *pointerInteraction = [[UIPointerInteraction alloc] initWithDelegate:self];
[self.view addInteraction:pointerInteraction];
GLFM_RELEASE(pointerInteraction);
}
#endif
}
#if TARGET_OS_OSX
- (void)viewDidLayout {
[super viewDidLayout];
// There doesn't appear to be a notification for insets changed on macOS.
if (@available(macOS 11, *)) {
double top, right, bottom, left;
glfmGetDisplayChromeInsets(self.glfmDisplay, &top, &right, &bottom, &left);
NSEdgeInsets newInsets = NSEdgeInsetsMake((CGFloat)top, (CGFloat)left, (CGFloat)bottom, (CGFloat)right);
if (!NSEdgeInsetsEqual(self.insets, newInsets)) {
self.insets = newInsets;
if (self.glfmDisplay->displayChromeInsetsChangedFunc) {
self.glfmDisplay->displayChromeInsetsChangedFunc(self.glfmDisplay, top, right, bottom, left);
}
}
}
}
#endif
#if TARGET_OS_IOS || TARGET_OS_TV
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
GLFMWindow *window = (GLFMWindow *)self.view.window;
self.glfmView.animating = window.active;
[self becomeFirstResponder];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
if (self.glfmDisplay->lowMemoryFunc) {
self.glfmDisplay->lowMemoryFunc(self.glfmDisplay);
}
}
- (void)viewSafeAreaInsetsDidChange {
[super viewSafeAreaInsetsDidChange];
if (self.glfmDisplay->displayChromeInsetsChangedFunc) {
double top, right, bottom, left;
glfmGetDisplayChromeInsets(self.glfmDisplay, &top, &right, &bottom, &left);
self.glfmDisplay->displayChromeInsetsChangedFunc(self.glfmDisplay, top, right, bottom, left);
}
}
#endif
#if TARGET_OS_IOS
- (UIView *)inputView {
if (self.keyboardRequested) {
return nil; // System keyboard
}
return self.noSoftKeyboardView;
}
- (BOOL)prefersStatusBarHidden {
return self.glfmDisplay->uiChrome != GLFMUserInterfaceChromeNavigationAndStatusBar;
}
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
UIRectEdge edges = UIRectEdgeLeft | UIRectEdgeRight;
return (self.glfmDisplay->uiChrome == GLFMUserInterfaceChromeNone ?
(UIRectEdgeBottom | edges) : edges);
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
GLFMInterfaceOrientation orientations = self.glfmDisplay->supportedOrientations;
BOOL portraitRequested = (orientations & (GLFMInterfaceOrientationPortrait | GLFMInterfaceOrientationPortraitUpsideDown)) != 0;
BOOL landscapeRequested = (orientations & GLFMInterfaceOrientationLandscape) != 0;
BOOL isTablet = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad;
if (portraitRequested && landscapeRequested) {
return isTablet ? UIInterfaceOrientationMaskAll : UIInterfaceOrientationMaskAllButUpsideDown;
}
if (portraitRequested) {
if (isTablet) {
return (UIInterfaceOrientationMask)(UIInterfaceOrientationMaskPortrait |
UIInterfaceOrientationMaskPortraitUpsideDown);
}
return UIInterfaceOrientationMaskPortrait;
}
return UIInterfaceOrientationMaskLandscape;
}
- (void)deviceOrientationChanged:(NSNotification *)notification {
UIInterfaceOrientation newOrientation = [[UIApplication sharedApplication] statusBarOrientation];
if (self.orientation != newOrientation) {
self.orientation = newOrientation;
[self.glfmViewIfLoaded requestRefresh];
if (self.glfmDisplay->orientationChangedFunc) {
self.glfmDisplay->orientationChangedFunc(self.glfmDisplay,
glfmGetInterfaceOrientation(self.glfmDisplay));
}
}
}
- (CMMotionManager *)motionManager {
if (!_motionManager) {
self.motionManager = GLFM_AUTORELEASE([CMMotionManager new]);
self.motionManager.deviceMotionUpdateInterval = 0.01;
}
return _motionManager;
}
- (BOOL)isMotionManagerLoaded {
return _motionManager != nil;
}
- (void)handleMotionEvents {
if (!self.isMotionManagerLoaded || !self.motionManager.isDeviceMotionActive) {
return;
}
CMDeviceMotion *deviceMotion = self.motionManager.deviceMotion;
if (!deviceMotion) {
// No readings yet
return;
}
GLFMSensorFunc accelerometerFunc = self.glfmDisplay->sensorFuncs[GLFMSensorAccelerometer];
if (accelerometerFunc) {
GLFMSensorEvent event = { 0 };
event.sensor = GLFMSensorAccelerometer;
event.timestamp = deviceMotion.timestamp;
event.vector.x = deviceMotion.userAcceleration.x + deviceMotion.gravity.x;
event.vector.y = deviceMotion.userAcceleration.y + deviceMotion.gravity.y;
event.vector.z = deviceMotion.userAcceleration.z + deviceMotion.gravity.z;
accelerometerFunc(self.glfmDisplay, event);
}
GLFMSensorFunc magnetometerFunc = self.glfmDisplay->sensorFuncs[GLFMSensorMagnetometer];
if (magnetometerFunc) {
GLFMSensorEvent event = { 0 };
event.sensor = GLFMSensorMagnetometer;
event.timestamp = deviceMotion.timestamp;
event.vector.x = deviceMotion.magneticField.field.x;
event.vector.y = deviceMotion.magneticField.field.y;
event.vector.z = deviceMotion.magneticField.field.z;
magnetometerFunc(self.glfmDisplay, event);
}
GLFMSensorFunc gyroscopeFunc = self.glfmDisplay->sensorFuncs[GLFMSensorGyroscope];
if (gyroscopeFunc) {
GLFMSensorEvent event = { 0 };
event.sensor = GLFMSensorGyroscope;
event.timestamp = deviceMotion.timestamp;
event.vector.x = deviceMotion.rotationRate.x;
event.vector.y = deviceMotion.rotationRate.y;
event.vector.z = deviceMotion.rotationRate.z;
gyroscopeFunc(self.glfmDisplay, event);
}
GLFMSensorFunc rotationFunc = self.glfmDisplay->sensorFuncs[GLFMSensorRotationMatrix];
if (rotationFunc) {
GLFMSensorEvent event = { 0 };
event.sensor = GLFMSensorRotationMatrix;
event.timestamp = deviceMotion.timestamp;
CMRotationMatrix matrix = deviceMotion.attitude.rotationMatrix;
event.matrix.m00 = matrix.m11; event.matrix.m01 = matrix.m12; event.matrix.m02 = matrix.m13;
event.matrix.m10 = matrix.m21; event.matrix.m11 = matrix.m22; event.matrix.m12 = matrix.m23;
event.matrix.m20 = matrix.m31; event.matrix.m21 = matrix.m32; event.matrix.m22 = matrix.m33;
rotationFunc(self.glfmDisplay, event);
}
}
- (void)updateMotionManagerActiveState {
BOOL enable = NO;
GLFMWindow *window = (GLFMWindow *)self.viewIfLoaded.window;
if (window.active) {
for (int i = 0; i < GLFM_NUM_SENSORS; i++) {
if (self.glfmDisplay->sensorFuncs[i] != NULL) {
enable = YES;
break;
}
}
}
if (enable && !self.motionManager.deviceMotionActive) {
CMAttitudeReferenceFrame referenceFrame;
CMAttitudeReferenceFrame availableReferenceFrames = [CMMotionManager availableAttitudeReferenceFrames];
if (availableReferenceFrames & CMAttitudeReferenceFrameXMagneticNorthZVertical) {
referenceFrame = CMAttitudeReferenceFrameXMagneticNorthZVertical;
} else if (availableReferenceFrames & CMAttitudeReferenceFrameXArbitraryCorrectedZVertical) {
referenceFrame = CMAttitudeReferenceFrameXArbitraryCorrectedZVertical;
} else {
referenceFrame = CMAttitudeReferenceFrameXArbitraryZVertical;
}
[self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:referenceFrame];
} else if (!enable && self.isMotionManagerLoaded && self.motionManager.deviceMotionActive) {
[self.motionManager stopDeviceMotionUpdates];
}
}
#endif // TARGET_OS_IOS
#if TARGET_OS_IOS || TARGET_OS_TV
// MARK: UIResponder
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)clearTouches {
for (int i = 0; i < GLFM_MAX_SIMULTANEOUS_TOUCHES; i++) {
activeTouches[i] = NULL;
}
}
- (void)addTouchEvent:(UITouch *)touch withType:(GLFMTouchPhase)phase {
int firstNullIndex = -1;
int index = -1;
for (int i = 0; i < GLFM_MAX_SIMULTANEOUS_TOUCHES; i++) {
if (activeTouches[i] == (__bridge const void *)touch) {
index = i;
break;
}
if (firstNullIndex == -1 && activeTouches[i] == NULL) {
firstNullIndex = i;
}
}
if (index == -1) {
if (firstNullIndex == -1) {
// Shouldn't happen
return;
}
index = firstNullIndex;
activeTouches[index] = (__bridge const void *)touch;
}
if (self.glfmDisplay->touchFunc) {
CGPoint currLocation = [touch locationInView:self.view];
currLocation.x *= self.view.contentScaleFactor;
currLocation.y *= self.view.contentScaleFactor;
self.glfmDisplay->touchFunc(self.glfmDisplay, index, phase,
(double)currLocation.x, (double)currLocation.y);
}
if (phase == GLFMTouchPhaseEnded || phase == GLFMTouchPhaseCancelled) {
activeTouches[index] = NULL;
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self addTouchEvent:touch withType:GLFMTouchPhaseBegan];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self addTouchEvent:touch withType:GLFMTouchPhaseMoved];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self addTouchEvent:touch withType:GLFMTouchPhaseEnded];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
[self addTouchEvent:touch withType:GLFMTouchPhaseCancelled];
}
}
#if TARGET_OS_IOS
- (void)hover:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.4)) {
if (self.glfmDisplay->touchFunc && (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged)) {
CGPoint currLocation = [recognizer locationInView:self.view];
currLocation.x *= self.view.contentScaleFactor;
currLocation.y *= self.view.contentScaleFactor;
self.glfmDisplay->touchFunc(self.glfmDisplay, 0, GLFMTouchPhaseHover,
(double)currLocation.x, (double)currLocation.y);
}
}
- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction
styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) {
switch (self.mouseCursor) {
case GLFMMouseCursorText:
return [UIPointerStyle styleWithShape:[UIPointerShape beamWithPreferredLength:20 axis:UIAxisVertical]
constrainedAxes:UIAxisNeither];
case GLFMMouseCursorVerticalText:
return [UIPointerStyle styleWithShape:[UIPointerShape beamWithPreferredLength:20 axis:UIAxisHorizontal]
constrainedAxes:UIAxisNeither];
case GLFMMouseCursorNone:
return [UIPointerStyle hiddenPointerStyle];
case GLFMMouseCursorCrosshair:
case GLFMMouseCursorDefault:
case GLFMMouseCursorAuto:
case GLFMMouseCursorPointer:
default:
return nil;
}
}
#endif
- (BOOL)handlePress:(UIPress *)press withAction:(GLFMKeyAction)action {
#if TARGET_OS_IOS
if (!self.glfmDisplay->keyFunc) {
return NO;
}
#elif TARGET_OS_TV
if (!self.glfmDisplay->keyFunc && !self.glfmDisplay->charFunc) {
return NO;
}
#endif
GLFMKeyCode keyCode = GLFMKeyCodeUnknown;
int modifierFlags = 0;
BOOL hasKey = NO;
BOOL isPrintable = NO;
if (@available(iOS 13.4, tvOS 13.4, *)) {
static const GLFMKeyCode HID_MAP[] = {
[UIKeyboardHIDUsageKeyboardReturnOrEnter] = GLFMKeyCodeEnter,
[UIKeyboardHIDUsageKeyboardTab] = GLFMKeyCodeTab,
[UIKeyboardHIDUsageKeyboardSpacebar] = GLFMKeyCodeSpace,
[UIKeyboardHIDUsageKeyboardDeleteOrBackspace] = GLFMKeyCodeBackspace,
[UIKeyboardHIDUsageKeyboardEscape] = GLFMKeyCodeEscape,
[UIKeyboardHIDUsageKeyboardCapsLock] = GLFMKeyCodeCapsLock,
[UIKeyboardHIDUsageKeyboardLeftGUI] = GLFMKeyCodeMetaLeft,
[UIKeyboardHIDUsageKeyboardLeftShift] = GLFMKeyCodeShiftLeft,
[UIKeyboardHIDUsageKeyboardLeftAlt] = GLFMKeyCodeAltLeft,
[UIKeyboardHIDUsageKeyboardLeftControl] = GLFMKeyCodeControlLeft,
[UIKeyboardHIDUsageKeyboardRightGUI] = GLFMKeyCodeMetaRight,
[UIKeyboardHIDUsageKeyboardRightShift] = GLFMKeyCodeShiftRight,
[UIKeyboardHIDUsageKeyboardRightAlt] = GLFMKeyCodeAltRight,
[UIKeyboardHIDUsageKeyboardRightControl] = GLFMKeyCodeControlRight,
[UIKeyboardHIDUsageKeyboardApplication] = GLFMKeyCodeMenu,
[UIKeyboardHIDUsageKeyboardPrintScreen] = GLFMKeyCodePrintScreen,
[UIKeyboardHIDUsageKeyboardScrollLock] = GLFMKeyCodeScrollLock,
[UIKeyboardHIDUsageKeyboardPause] = GLFMKeyCodePause,
[UIKeyboardHIDUsageKeyboardInsert] = GLFMKeyCodeInsert,
[UIKeyboardHIDUsageKeyboardHome] = GLFMKeyCodeHome,
[UIKeyboardHIDUsageKeyboardPageUp] = GLFMKeyCodePageUp,
[UIKeyboardHIDUsageKeyboardDeleteForward] = GLFMKeyCodeDelete,
[UIKeyboardHIDUsageKeyboardEnd] = GLFMKeyCodeEnd,
[UIKeyboardHIDUsageKeyboardPageDown] = GLFMKeyCodePageDown,
[UIKeyboardHIDUsageKeyboardLeftArrow] = GLFMKeyCodeArrowLeft,
[UIKeyboardHIDUsageKeyboardRightArrow] = GLFMKeyCodeArrowRight,
[UIKeyboardHIDUsageKeyboardDownArrow] = GLFMKeyCodeArrowDown,
[UIKeyboardHIDUsageKeyboardUpArrow] = GLFMKeyCodeArrowUp,
[UIKeyboardHIDUsageKeyboardEqualSign] = GLFMKeyCodeEqual,
[UIKeyboardHIDUsageKeyboardHyphen] = GLFMKeyCodeMinus,
[UIKeyboardHIDUsageKeyboardOpenBracket] = GLFMKeyCodeBracketLeft,
[UIKeyboardHIDUsageKeyboardCloseBracket] = GLFMKeyCodeBracketRight,
[UIKeyboardHIDUsageKeyboardQuote] = GLFMKeyCodeQuote,
[UIKeyboardHIDUsageKeyboardSemicolon] = GLFMKeyCodeSemicolon,
[UIKeyboardHIDUsageKeyboardBackslash] = GLFMKeyCodeBackslash,
[UIKeyboardHIDUsageKeyboardComma] = GLFMKeyCodeComma,
[UIKeyboardHIDUsageKeyboardSlash] = GLFMKeyCodeSlash,
[UIKeyboardHIDUsageKeyboardPeriod] = GLFMKeyCodePeriod,
[UIKeyboardHIDUsageKeyboardGraveAccentAndTilde] = GLFMKeyCodeBackquote,
[UIKeyboardHIDUsageKeyboardA] = GLFMKeyCodeA,
[UIKeyboardHIDUsageKeyboardB] = GLFMKeyCodeB,
[UIKeyboardHIDUsageKeyboardC] = GLFMKeyCodeC,
[UIKeyboardHIDUsageKeyboardD] = GLFMKeyCodeD,
[UIKeyboardHIDUsageKeyboardE] = GLFMKeyCodeE,
[UIKeyboardHIDUsageKeyboardF] = GLFMKeyCodeF,
[UIKeyboardHIDUsageKeyboardG] = GLFMKeyCodeG,
[UIKeyboardHIDUsageKeyboardH] = GLFMKeyCodeH,
[UIKeyboardHIDUsageKeyboardI] = GLFMKeyCodeI,
[UIKeyboardHIDUsageKeyboardJ] = GLFMKeyCodeJ,
[UIKeyboardHIDUsageKeyboardK] = GLFMKeyCodeK,
[UIKeyboardHIDUsageKeyboardL] = GLFMKeyCodeL,
[UIKeyboardHIDUsageKeyboardN] = GLFMKeyCodeN,
[UIKeyboardHIDUsageKeyboardM] = GLFMKeyCodeM,
[UIKeyboardHIDUsageKeyboardO] = GLFMKeyCodeO,
[UIKeyboardHIDUsageKeyboardP] = GLFMKeyCodeP,
[UIKeyboardHIDUsageKeyboardQ] = GLFMKeyCodeQ,
[UIKeyboardHIDUsageKeyboardR] = GLFMKeyCodeR,
[UIKeyboardHIDUsageKeyboardS] = GLFMKeyCodeS,
[UIKeyboardHIDUsageKeyboardT] = GLFMKeyCodeT,
[UIKeyboardHIDUsageKeyboardU] = GLFMKeyCodeU,
[UIKeyboardHIDUsageKeyboardV] = GLFMKeyCodeV,
[UIKeyboardHIDUsageKeyboardW] = GLFMKeyCodeW,
[UIKeyboardHIDUsageKeyboardX] = GLFMKeyCodeX,
[UIKeyboardHIDUsageKeyboardY] = GLFMKeyCodeY,
[UIKeyboardHIDUsageKeyboardZ] = GLFMKeyCodeZ,
[UIKeyboardHIDUsageKeyboard0] = GLFMKeyCode0,
[UIKeyboardHIDUsageKeyboard1] = GLFMKeyCode1,
[UIKeyboardHIDUsageKeyboard2] = GLFMKeyCode2,
[UIKeyboardHIDUsageKeyboard3] = GLFMKeyCode3,
[UIKeyboardHIDUsageKeyboard4] = GLFMKeyCode4,
[UIKeyboardHIDUsageKeyboard5] = GLFMKeyCode5,
[UIKeyboardHIDUsageKeyboard6] = GLFMKeyCode6,
[UIKeyboardHIDUsageKeyboard7] = GLFMKeyCode7,
[UIKeyboardHIDUsageKeyboard8] = GLFMKeyCode8,
[UIKeyboardHIDUsageKeyboard9] = GLFMKeyCode9,
[UIKeyboardHIDUsageKeyboardPower] = GLFMKeyCodePower,
[UIKeyboardHIDUsageKeypadNumLock] = GLFMKeyCodeNumLock,
[UIKeyboardHIDUsageKeypadPeriod] = GLFMKeyCodeNumpadDecimal,
[UIKeyboardHIDUsageKeypadAsterisk] = GLFMKeyCodeNumpadMultiply,
[UIKeyboardHIDUsageKeypadPlus] = GLFMKeyCodeNumpadAdd,
[UIKeyboardHIDUsageKeypadSlash] = GLFMKeyCodeNumpadDivide,
[UIKeyboardHIDUsageKeypadEnter] = GLFMKeyCodeNumpadEnter,
[UIKeyboardHIDUsageKeypadHyphen] = GLFMKeyCodeNumpadSubtract,
[UIKeyboardHIDUsageKeypadEqualSign] = GLFMKeyCodeNumpadEqual,
[UIKeyboardHIDUsageKeypad0] = GLFMKeyCodeNumpad0,
[UIKeyboardHIDUsageKeypad1] = GLFMKeyCodeNumpad1,
[UIKeyboardHIDUsageKeypad2] = GLFMKeyCodeNumpad2,
[UIKeyboardHIDUsageKeypad3] = GLFMKeyCodeNumpad3,
[UIKeyboardHIDUsageKeypad4] = GLFMKeyCodeNumpad4,
[UIKeyboardHIDUsageKeypad5] = GLFMKeyCodeNumpad5,
[UIKeyboardHIDUsageKeypad6] = GLFMKeyCodeNumpad6,
[UIKeyboardHIDUsageKeypad7] = GLFMKeyCodeNumpad7,
[UIKeyboardHIDUsageKeypad8] = GLFMKeyCodeNumpad8,
[UIKeyboardHIDUsageKeypad9] = GLFMKeyCodeNumpad9,
[UIKeyboardHIDUsageKeyboardF1] = GLFMKeyCodeF1,
[UIKeyboardHIDUsageKeyboardF2] = GLFMKeyCodeF2,
[UIKeyboardHIDUsageKeyboardF3] = GLFMKeyCodeF3,
[UIKeyboardHIDUsageKeyboardF4] = GLFMKeyCodeF4,
[UIKeyboardHIDUsageKeyboardF5] = GLFMKeyCodeF5,
[UIKeyboardHIDUsageKeyboardF6] = GLFMKeyCodeF6,
[UIKeyboardHIDUsageKeyboardF7] = GLFMKeyCodeF7,
[UIKeyboardHIDUsageKeyboardF8] = GLFMKeyCodeF8,
[UIKeyboardHIDUsageKeyboardF9] = GLFMKeyCodeF9,
[UIKeyboardHIDUsageKeyboardF10] = GLFMKeyCodeF10,
[UIKeyboardHIDUsageKeyboardF11] = GLFMKeyCodeF11,
[UIKeyboardHIDUsageKeyboardF12] = GLFMKeyCodeF12,
[UIKeyboardHIDUsageKeyboardF13] = GLFMKeyCodeF13,
[UIKeyboardHIDUsageKeyboardF14] = GLFMKeyCodeF14,
[UIKeyboardHIDUsageKeyboardF15] = GLFMKeyCodeF15,
[UIKeyboardHIDUsageKeyboardF16] = GLFMKeyCodeF16,
[UIKeyboardHIDUsageKeyboardF17] = GLFMKeyCodeF17,
[UIKeyboardHIDUsageKeyboardF18] = GLFMKeyCodeF18,
[UIKeyboardHIDUsageKeyboardF19] = GLFMKeyCodeF19,
[UIKeyboardHIDUsageKeyboardF20] = GLFMKeyCodeF20,
[UIKeyboardHIDUsageKeyboardF21] = GLFMKeyCodeF21,
[UIKeyboardHIDUsageKeyboardF22] = GLFMKeyCodeF22,
[UIKeyboardHIDUsageKeyboardF23] = GLFMKeyCodeF23,
[UIKeyboardHIDUsageKeyboardF24] = GLFMKeyCodeF24,
};
UIKey *key = press.key;
if (key) {
BOOL isControlKey = NO;
hasKey = YES;
if ((key.modifierFlags & UIKeyModifierShift) != 0) {
modifierFlags |= GLFMKeyModifierShift;
}
if ((key.modifierFlags & UIKeyModifierControl) != 0) {
modifierFlags |= GLFMKeyModifierControl;
isControlKey = YES;
}
if ((key.modifierFlags & UIKeyModifierAlternate) != 0) {
modifierFlags |= GLFMKeyModifierAlt;
}
if ((key.modifierFlags & UIKeyModifierCommand) != 0) {
modifierFlags |= GLFMKeyModifierMeta;
isControlKey = YES;
}
if (key.keyCode >= 0 && (size_t)key.keyCode < sizeof(HID_MAP) / sizeof(*HID_MAP)) {
keyCode = HID_MAP[key.keyCode];
}
if (self.isFirstResponder && self.glfmDisplay->charFunc != NULL &&
action != GLFMKeyActionReleased && !isControlKey &&
keyCode >= GLFMKeyCodeSpace && keyCode != GLFMKeyCodeDelete) {
NSString *chars = key.charactersIgnoringModifiers;
isPrintable = (chars.length > 0 && [chars characterAtIndex:0] >= ' ' &&
![chars hasPrefix:@"UIKeyInput"]);
}
}
}
// NOTE: iOS supports UIKeyInput, but tvOS does not. Handle them separately.
#if TARGET_OS_IOS
if (self.isFirstResponder &&
(keyCode == GLFMKeyCodeEnter || keyCode == GLFMKeyCodeTab || keyCode == GLFMKeyCodeBackspace)) {
// Let UIKeyInput handle these keys via insertText and deleteBackwards (allow key repeating).
return NO;
}
if (keyCode == GLFMKeyCodeUnknown && !hasKey) {
// The tab key on the Magic Keyboard sends two UIPress events. For the second one, press.key=nil and press.type=0xcb.
return NO;
}
BOOL handled = self.glfmDisplay->keyFunc(self.glfmDisplay, keyCode, action, modifierFlags);
if (self.isFirstResponder && isPrintable && self.glfmDisplay->charFunc) {
// Send text via insertText.
return NO;
}
return handled;
#endif // TARGET_OS_IOS
#if TARGET_OS_TV
if (keyCode == GLFMKeyCodeUnknown) {
switch (press.type) {
case UIPressTypeUpArrow:
keyCode = GLFMKeyCodeArrowUp;
break;
case UIPressTypeDownArrow:
keyCode = GLFMKeyCodeArrowDown;
break;
case UIPressTypeLeftArrow:
keyCode = GLFMKeyCodeArrowLeft;
break;
case UIPressTypeRightArrow:
keyCode = GLFMKeyCodeArrowRight;
break;
case UIPressTypeSelect:
keyCode = GLFMKeyCodeMediaSelect;
break;
case UIPressTypeMenu:
keyCode = GLFMKeyCodeNavigationBack;
break;
case UIPressTypePlayPause:
keyCode = GLFMKeyCodeMediaPlayPause;
break;
case UIPressTypePageUp:
keyCode = GLFMKeyCodePageUp;
break;
case UIPressTypePageDown:
keyCode = GLFMKeyCodePageDown;
break;
}
}
BOOL handled = NO;
if (self.glfmDisplay->keyFunc) {
handled = self.glfmDisplay->keyFunc(self.glfmDisplay, keyCode, action, modifierFlags);
}
if (@available(iOS 13.4, tvOS 13.4, *)) {
if (self.isFirstResponder && hasKey && isPrintable && self.glfmDisplay->charFunc) {
self.glfmDisplay->charFunc(self.glfmDisplay, press.key.characters.UTF8String, 0);
}
}
return handled;
#endif // TARGET_OS_TV
}
// Returns set of unhandled presses
- (NSSet<UIPress *> *)handlePresses:(NSSet<UIPress *> *)presses withAction:(GLFMKeyAction)action {
NSMutableSet<UIPress *> *unhandledPresses = nil;
for (UIPress *press in presses) {
BOOL handled = [self handlePress:press withAction:action];
if (!handled) {
if (presses.count == 1) {
return presses; // Likely case
}
if (!unhandledPresses) {
unhandledPresses = [NSMutableSet set];
}
[unhandledPresses addObject:press];
}
}
return unhandledPresses;
}
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
NSSet<UIPress *> *unhandledPresses = [self handlePresses:presses withAction:GLFMKeyActionPressed];
if (unhandledPresses.count > 0) {
[super pressesBegan:unhandledPresses withEvent:event];
}
}
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
[super pressesChanged:presses withEvent:event];
}
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
NSSet<UIPress *> *unhandledPresses = [self handlePresses:presses withAction:GLFMKeyActionReleased];
if (unhandledPresses.count > 0) {
[super pressesEnded:unhandledPresses withEvent:event];
}
}
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
NSSet<UIPress *> *unhandledPresses = [self handlePresses:presses withAction:GLFMKeyActionReleased];
if (unhandledPresses.count > 0) {
[super pressesCancelled:unhandledPresses withEvent:event];
}
}
// MARK: UIKeyInput
#if TARGET_OS_IOS
- (void)keyboardFrameWillChange:(NSNotification *)notification {
NSObject *frameBeginValue = notification.userInfo[UIKeyboardFrameBeginUserInfoKey];
NSObject *frameEndValue = notification.userInfo[UIKeyboardFrameEndUserInfoKey];
if ([frameBeginValue isKindOfClass:[NSValue class]] &&
[frameEndValue isKindOfClass:[NSValue class]]) {
CGRect keyboardFrameBegin = [(NSValue *)frameBeginValue CGRectValue];
CGRect keyboardFrameEnd = [(NSValue *)frameEndValue CGRectValue];
// If height is zero, the keyboard is hidden because a physical keyboard was attached.
BOOL keyboardWasVisible = CGRectIntersectsRect(self.view.window.frame, keyboardFrameBegin);
BOOL keyboardIsVisible = CGRectIntersectsRect(self.view.window.frame, keyboardFrameEnd);
if (keyboardWasVisible && !keyboardIsVisible && keyboardFrameEnd.size.height > (CGFloat)0.0) {
// User hid keyboard (iPad)
self.keyboardRequested = NO;
}
[self.glfmViewIfLoaded requestRefresh];
if (self.glfmDisplay->keyboardVisibilityChangedFunc) {
// Convert to view coordinates
keyboardFrameEnd = [self.view convertRect:keyboardFrameEnd fromView:nil];
// Convert to pixels
keyboardFrameEnd.origin.x *= self.view.contentScaleFactor;
keyboardFrameEnd.origin.y *= self.view.contentScaleFactor;
keyboardFrameEnd.size.width *= self.view.contentScaleFactor;
keyboardFrameEnd.size.height *= self.view.contentScaleFactor;
self.glfmDisplay->keyboardVisibilityChangedFunc(self.glfmDisplay, keyboardIsVisible,
(double)keyboardFrameEnd.origin.x,
(double)keyboardFrameEnd.origin.y,
(double)keyboardFrameEnd.size.width,
(double)keyboardFrameEnd.size.height);
}
}
}
// UITextInputTraits - disable suggestion bar
- (UITextAutocorrectionType)autocorrectionType {
return UITextAutocorrectionTypeNo;
}
- (BOOL)hasText {
return YES;
}
- (void)insertText:(NSString *)text {
if ([text isEqualToString:@"\n"]) {
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, GLFMKeyCodeEnter, GLFMKeyActionPressed, 0);
}
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, GLFMKeyCodeEnter, GLFMKeyActionReleased, 0);
}
} else if ([text isEqualToString:@"\t"]) {
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, GLFMKeyCodeTab, GLFMKeyActionPressed, 0);
}
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, GLFMKeyCodeTab, GLFMKeyActionReleased, 0);
}
} else if (self.glfmDisplay->charFunc) {
self.glfmDisplay->charFunc(self.glfmDisplay, text.UTF8String, 0);
}
}
- (void)deleteBackward {
// NOTE: This method is called for key repeat events when using a hardware keyboard, but not
// when using the software keyboard.
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, GLFMKeyCodeBackspace, GLFMKeyActionPressed, 0);
}
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, GLFMKeyCodeBackspace, GLFMKeyActionReleased, 0);
}
}
#endif // TARGET_OS_IOS
- (NSArray<UIKeyCommand *> *)keyCommands {
if (@available(iOS 13.4, tvOS 13.4, *)) {
// Using UIPress events
return @[];
}
static NSArray<UIKeyCommand *> *keyCommands = NULL;
if (!keyCommands) {
NSArray<NSString *> *keyInputs = @[
UIKeyInputUpArrow, UIKeyInputDownArrow, UIKeyInputLeftArrow, UIKeyInputRightArrow,
UIKeyInputEscape, UIKeyInputPageUp, UIKeyInputPageDown,
];
NSMutableArray *mutableKeyCommands = GLFM_AUTORELEASE([NSMutableArray new]);
[keyInputs enumerateObjectsUsingBlock:^(NSString *keyInput, NSUInteger idx, BOOL *stop) {
(void)idx;
(void)stop;
[mutableKeyCommands addObject:[UIKeyCommand keyCommandWithInput:keyInput
modifierFlags:(UIKeyModifierFlags)0
action:@selector(keyPressed:)]];
}];
keyCommands = [mutableKeyCommands copy];
}
return keyCommands;
}
- (void)keyPressed:(UIKeyCommand *)keyCommand {
// Only invoked on iOS/tvOS 13.3 and older
NSString *key = keyCommand.input;
GLFMKeyCode keyCode = GLFMKeyCodeUnknown;
if (key == UIKeyInputUpArrow) {
keyCode = GLFMKeyCodeArrowUp;
} else if (key == UIKeyInputDownArrow) {
keyCode = GLFMKeyCodeArrowDown;
} else if (key == UIKeyInputLeftArrow) {
keyCode = GLFMKeyCodeArrowLeft;
} else if (key == UIKeyInputRightArrow) {
keyCode = GLFMKeyCodeArrowRight;
} else if (key == UIKeyInputEscape) {
keyCode = GLFMKeyCodeEscape;
} else if (key == UIKeyInputPageUp) {
keyCode = GLFMKeyCodePageUp;
} else if (key == UIKeyInputPageDown) {
keyCode = GLFMKeyCodePageDown;
}
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, keyCode, GLFMKeyActionPressed, 0);
}
if (self.glfmDisplay->keyFunc) {
self.glfmDisplay->keyFunc(self.glfmDisplay, keyCode, GLFMKeyActionReleased, 0);
}
}
#endif // TARGET_OS_IOS || TARGET_OS_TV
#if TARGET_OS_OSX
// MARK: NSResponder (Mouse)
// Returns NO if location could not be determined
- (BOOL)getLocationForEvent:(NSEvent *)event outX:(double *)outX outY:(double *)outY {
NSWindow *window = event.window;
CGPoint locationInWindow;
if (window) {
locationInWindow = event.locationInWindow;
} else {
window = self.view.window;
if (!window) {
return NO;
}
// When event.window is nil, the locationInWindow property "contains the
// event location in screen coordinates."
CGPoint locationInScreen = event.locationInWindow;
locationInWindow = [window convertPointFromScreen:locationInScreen];
}
CGFloat scale = window.backingScaleFactor;
CGFloat contentHeight = window.contentLayoutRect.size.height;
*outX = locationInWindow.x * scale;
*outY = (contentHeight - locationInWindow.y) * scale; // Flip y
return YES;
}
- (void)sendMouseEvent:(NSEvent *)event withType:(GLFMTouchPhase)phase {
if (!self.glfmDisplay->touchFunc) {
return;
}
double x, y;
if (![self getLocationForEvent:event outX:&x outY:&y]) {
return;
}
if (phase == GLFMTouchPhaseEnded) {
GLFMWindow *window = (GLFMWindow *)self.glfmViewIfLoaded.window;
if (!window.active) {
phase = GLFMTouchPhaseCancelled;
}
}
self.glfmDisplay->touchFunc(self.glfmDisplay, (int)event.buttonNumber, phase, x, y);
}
- (void)mouseMoved:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseHover];
}
- (void)mouseDown:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseBegan];
}
- (void)mouseDragged:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseMoved];
}
- (void)mouseUp:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseEnded];
}
- (void)rightMouseDown:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseBegan];
}
- (void)rightMouseDragged:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseMoved];
}
- (void)rightMouseUp:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseEnded];
}
- (void)otherMouseDown:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseBegan];
}
- (void)otherMouseDragged:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseMoved];
}
- (void)otherMouseUp:(NSEvent *)event {
[self sendMouseEvent:event withType:GLFMTouchPhaseEnded];
}
- (void)scrollWheel:(NSEvent *)event {
if (!self.glfmDisplay->mouseWheelFunc) {
return;
}
double x, y;
if (![self getLocationForEvent:event outX:&x outY:&y]) {
return;
}
// Invert to match web browser behavior
double deltaX = -event.scrollingDeltaX;
double deltaY = -event.scrollingDeltaY;
GLFMMouseWheelDeltaType deltaType = (event.hasPreciseScrollingDeltas ? GLFMMouseWheelDeltaPixel
: GLFMMouseWheelDeltaLine);
self.glfmDisplay->mouseWheelFunc(self.glfmDisplay, x, y, deltaType, deltaX, deltaY, 0.0);
}
- (void)cursorUpdate:(NSEvent *)event {
if (event.trackingArea) {
[self.currentCursor set];
} else {
[super cursorUpdate:event];
}
}
- (void)mouseEntered:(NSEvent *)event {
self.mouseInside = YES;
}
- (void)mouseExited:(NSEvent *)event {
self.mouseInside = NO;
[NSCursor.arrowCursor set];
}
// MARK: NSResponder (Keyboard)
- (void)setCurrentKeyboard {
if (self.currentKeyboard) {
CFRelease(self.currentKeyboard);
}
self.currentKeyboard = TISCopyCurrentKeyboardInputSource();
CFDataRef layoutData = TISGetInputSourceProperty(self.currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
if (layoutData) {
self.keyboardLayout = (const UCKeyboardLayout *)(const void *)CFDataGetBytePtr(layoutData);
} else {
self.keyboardLayout = NULL;
}
}
- (void)keyboardChanged:(NSObject *)object {
[self setCurrentKeyboard];
}
- (BOOL)sendKeyEvent:(NSEvent *)event withAction:(GLFMKeyAction)action {
BOOL handled = NO;
// Send key event
if (self.glfmDisplay->keyFunc) {
static const GLFMKeyCode VK_MAP[] = {
[kVK_Return] = GLFMKeyCodeEnter,
[kVK_Tab] = GLFMKeyCodeTab,
[kVK_Space] = GLFMKeyCodeSpace,
[kVK_Delete] = GLFMKeyCodeBackspace,
[kVK_Escape] = GLFMKeyCodeEscape,
[kVK_Command] = GLFMKeyCodeMetaLeft,
[kVK_Shift] = GLFMKeyCodeShiftLeft,
[kVK_CapsLock] = GLFMKeyCodeCapsLock,
[kVK_Option] = GLFMKeyCodeAltLeft,
[kVK_Control] = GLFMKeyCodeControlLeft,
[kVK_RightCommand] = GLFMKeyCodeMetaRight,
[kVK_RightShift] = GLFMKeyCodeShiftRight,
[kVK_RightOption] = GLFMKeyCodeAltRight,
[kVK_RightControl] = GLFMKeyCodeControlRight,
[kVK_Function] = GLFMKeyCodeFunction,
[kVK_Help] = GLFMKeyCodeInsert,
[kVK_Home] = GLFMKeyCodeHome,
[kVK_PageUp] = GLFMKeyCodePageUp,
[kVK_ForwardDelete] = GLFMKeyCodeDelete,
[kVK_End] = GLFMKeyCodeEnd,
[kVK_PageDown] = GLFMKeyCodePageDown,
[kVK_LeftArrow] = GLFMKeyCodeArrowLeft,
[kVK_RightArrow] = GLFMKeyCodeArrowRight,
[kVK_DownArrow] = GLFMKeyCodeArrowDown,
[kVK_UpArrow] = GLFMKeyCodeArrowUp,
[kVK_ANSI_A] = GLFMKeyCodeA,
[kVK_ANSI_B] = GLFMKeyCodeB,
[kVK_ANSI_C] = GLFMKeyCodeC,
[kVK_ANSI_D] = GLFMKeyCodeD,
[kVK_ANSI_E] = GLFMKeyCodeE,
[kVK_ANSI_F] = GLFMKeyCodeF,
[kVK_ANSI_G] = GLFMKeyCodeG,
[kVK_ANSI_H] = GLFMKeyCodeH,
[kVK_ANSI_I] = GLFMKeyCodeI,
[kVK_ANSI_J] = GLFMKeyCodeJ,
[kVK_ANSI_K] = GLFMKeyCodeK,
[kVK_ANSI_L] = GLFMKeyCodeL,
[kVK_ANSI_N] = GLFMKeyCodeN,
[kVK_ANSI_M] = GLFMKeyCodeM,
[kVK_ANSI_O] = GLFMKeyCodeO,
[kVK_ANSI_P] = GLFMKeyCodeP,
[kVK_ANSI_Q] = GLFMKeyCodeQ,
[kVK_ANSI_R] = GLFMKeyCodeR,
[kVK_ANSI_S] = GLFMKeyCodeS,
[kVK_ANSI_T] = GLFMKeyCodeT,
[kVK_ANSI_U] = GLFMKeyCodeU,
[kVK_ANSI_V] = GLFMKeyCodeV,
[kVK_ANSI_W] = GLFMKeyCodeW,
[kVK_ANSI_X] = GLFMKeyCodeX,
[kVK_ANSI_Y] = GLFMKeyCodeY,
[kVK_ANSI_Z] = GLFMKeyCodeZ,
[kVK_ANSI_0] = GLFMKeyCode0,
[kVK_ANSI_1] = GLFMKeyCode1,
[kVK_ANSI_2] = GLFMKeyCode2,
[kVK_ANSI_3] = GLFMKeyCode3,
[kVK_ANSI_4] = GLFMKeyCode4,
[kVK_ANSI_5] = GLFMKeyCode5,
[kVK_ANSI_6] = GLFMKeyCode6,
[kVK_ANSI_7] = GLFMKeyCode7,
[kVK_ANSI_8] = GLFMKeyCode8,
[kVK_ANSI_9] = GLFMKeyCode9,
[kVK_ANSI_Equal] = GLFMKeyCodeEqual,
[kVK_ANSI_Minus] = GLFMKeyCodeMinus,
[kVK_ANSI_RightBracket] = GLFMKeyCodeBracketRight,
[kVK_ANSI_LeftBracket] = GLFMKeyCodeBracketLeft,
[kVK_ANSI_Quote] = GLFMKeyCodeQuote,
[kVK_ANSI_Semicolon] = GLFMKeyCodeSemicolon,
[kVK_ANSI_Backslash] = GLFMKeyCodeBackslash,
[kVK_ANSI_Comma] = GLFMKeyCodeComma,
[kVK_ANSI_Slash] = GLFMKeyCodeSlash,
[kVK_ANSI_Period] = GLFMKeyCodePeriod,
[kVK_ANSI_Grave] = GLFMKeyCodeBackquote,
[kVK_ANSI_KeypadClear] = GLFMKeyCodeNumLock,
[kVK_ANSI_KeypadDecimal] = GLFMKeyCodeNumpadDecimal,
[kVK_ANSI_KeypadMultiply] = GLFMKeyCodeNumpadMultiply,
[kVK_ANSI_KeypadPlus] = GLFMKeyCodeNumpadAdd,
[kVK_ANSI_KeypadDivide] = GLFMKeyCodeNumpadDivide,
[kVK_ANSI_KeypadEnter] = GLFMKeyCodeNumpadEnter,
[kVK_ANSI_KeypadMinus] = GLFMKeyCodeNumpadSubtract,
[kVK_ANSI_KeypadEquals] = GLFMKeyCodeNumpadEqual,
[kVK_ANSI_Keypad0] = GLFMKeyCodeNumpad0,
[kVK_ANSI_Keypad1] = GLFMKeyCodeNumpad1,
[kVK_ANSI_Keypad2] = GLFMKeyCodeNumpad2,
[kVK_ANSI_Keypad3] = GLFMKeyCodeNumpad3,
[kVK_ANSI_Keypad4] = GLFMKeyCodeNumpad4,
[kVK_ANSI_Keypad5] = GLFMKeyCodeNumpad5,
[kVK_ANSI_Keypad6] = GLFMKeyCodeNumpad6,
[kVK_ANSI_Keypad7] = GLFMKeyCodeNumpad7,
[kVK_ANSI_Keypad8] = GLFMKeyCodeNumpad8,
[kVK_ANSI_Keypad9] = GLFMKeyCodeNumpad9,
[kVK_F1] = GLFMKeyCodeF1,
[kVK_F2] = GLFMKeyCodeF2,
[kVK_F3] = GLFMKeyCodeF3,
[kVK_F4] = GLFMKeyCodeF4,
[kVK_F5] = GLFMKeyCodeF5,
[kVK_F6] = GLFMKeyCodeF6,
[kVK_F7] = GLFMKeyCodeF7,
[kVK_F8] = GLFMKeyCodeF8,
[kVK_F9] = GLFMKeyCodeF9,
[kVK_F10] = GLFMKeyCodeF10,
[kVK_F11] = GLFMKeyCodeF11,
[kVK_F12] = GLFMKeyCodeF12,
[kVK_F13] = GLFMKeyCodeF13,
[kVK_F14] = GLFMKeyCodeF14,
[kVK_F15] = GLFMKeyCodeF15,
[kVK_F16] = GLFMKeyCodeF16,
[kVK_F17] = GLFMKeyCodeF17,
[kVK_F18] = GLFMKeyCodeF18,
[kVK_F19] = GLFMKeyCodeF19,
[kVK_F20] = GLFMKeyCodeF20,
[0x6e] = GLFMKeyCodeMenu,
};
GLFMKeyCode keyCode = GLFMKeyCodeUnknown;
if (event.keyCode < sizeof(VK_MAP) / sizeof(*VK_MAP)) {
keyCode = VK_MAP[event.keyCode];
}
int modifiers = 0;
if ((event.modifierFlags & NSEventModifierFlagShift) != 0) {
modifiers |= GLFMKeyModifierShift;
}
if ((event.modifierFlags & NSEventModifierFlagControl) != 0) {
modifiers |= GLFMKeyModifierControl;
}
if ((event.modifierFlags & NSEventModifierFlagOption) != 0) {
modifiers |= GLFMKeyModifierAlt;
}
if ((event.modifierFlags & NSEventModifierFlagCommand) != 0) {
modifiers |= GLFMKeyModifierMeta;
}
if (self.fnModifier) {
modifiers |= GLFMKeyModifierFunction;
}
handled = self.glfmDisplay->keyFunc(self.glfmDisplay, keyCode, action, modifiers);
}
// Send char event
if (self.glfmDisplay->charFunc &&
event.type == NSEventTypeKeyDown &&
(event.modifierFlags & NSEventModifierFlagFunction) == 0 &&
(event.modifierFlags & NSEventModifierFlagCommand) == 0 &&
(event.modifierFlags & NSEventModifierFlagControl) == 0) {
UniChar utf16[4];
char utf8[4 * 2 + 1] = { 0 };
// This is like `event.characters`, but considers dead key state and ignores control codes.
// Sets `utf8` on success.
if (self.keyboardLayout) {
UInt32 modifierKeyState = (event.modifierFlags >> 16) & 0xFF;
UniCharCount utf16Length = 0;
OSStatus status = UCKeyTranslate(self.keyboardLayout, event.keyCode, kUCKeyActionDown, modifierKeyState,
LMGetKbdType(), 0, &_deadKeyState, sizeof(utf16) / sizeof(*utf16), &utf16Length, utf16);
if (status == noErr && utf16Length > 0) {
UniChar firstChar = utf16[0];
BOOL isControlCode = (firstChar < 0x20 ||
firstChar == NSDeleteCharacter ||
firstChar == NSLineSeparatorCharacter ||
firstChar == NSParagraphSeparatorCharacter);
if (!isControlCode) {
// Convert to UTF8
CFStringRef string = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, utf16, (CFIndex)utf16Length, kCFAllocatorNull);
CFStringGetCString(string, utf8, sizeof(utf8), kCFStringEncodingUTF8);
CFRelease(string);
}
}
}
if (utf8[0] != '\0') {
if (self.hideMouseCursorWhileTyping) {
[NSCursor setHiddenUntilMouseMoves:YES];
}
self.glfmDisplay->charFunc(self.glfmDisplay, utf8, 0);
}
}
return handled;
}
- (void)keyDown:(NSEvent *)event {
GLFMKeyAction action = (event.isARepeat ? GLFMKeyActionRepeated : GLFMKeyActionPressed);
BOOL handled = [self sendKeyEvent:event withAction:action];
if (!handled) {
// Interpret unhandled keys. For now, only interpretation is "Esc" to exit fullscreen.
if (event.keyCode == kVK_Escape) {
NSWindow *window = self.glfmViewIfLoaded.window;
if (window && (window.styleMask & NSWindowStyleMaskFullScreen) != 0) {
[window toggleFullScreen:nil];
}
}
}
}
- (void)keyUp:(NSEvent *)event {
[self sendKeyEvent:event withAction:GLFMKeyActionReleased];
}
- (void)flagsChanged:(NSEvent *)event {
NSEventModifierFlags modifier = 0;
switch (event.keyCode) {
case kVK_Command: modifier = NX_DEVICELCMDKEYMASK; break;
case kVK_Shift: modifier = NX_DEVICELSHIFTKEYMASK; break;
case kVK_CapsLock: modifier = NSEventModifierFlagCapsLock; break;
case kVK_Option: modifier = NX_DEVICELALTKEYMASK; break;
case kVK_Control: modifier = NX_DEVICELCTLKEYMASK; break;
case kVK_RightCommand: modifier = NX_DEVICERCMDKEYMASK; break;
case kVK_RightShift: modifier = NX_DEVICERSHIFTKEYMASK; break;
case kVK_RightOption: modifier = NX_DEVICERALTKEYMASK; break;
case kVK_RightControl: modifier = NX_DEVICERCTLKEYMASK; break;
case kVK_Function: modifier = NSEventModifierFlagFunction; break;
}
if (modifier != 0) {
BOOL pressed = ((event.modifierFlags & modifier) != 0);
if (event.keyCode == kVK_Function) {
// The fn key cannot be determined from `modifierFlags` because the
// `NSEventModifierFlagFunction` flag is also used for other keys, like the arrow keys.
// So, keep track of its state.
self.fnModifier = pressed;
}
GLFMKeyAction action = pressed ? GLFMKeyActionPressed : GLFMKeyActionReleased;
[self sendKeyEvent:event withAction:action];
}
}
- (void)clearActiveKeys {
self.fnModifier = NO;
self.deadKeyState = 0;
}
#endif // TARGET_OS_OSX
@end // GLFMViewController
// MARK: - GLFMWindow implementation
@implementation GLFMWindow
@synthesize active = _active;
- (void)setActive:(BOOL)active {
if (_active != active) {
_active = active;
#if TARGET_OS_OSX
GLFMViewController *viewController = (GLFMViewController *)self.contentViewController;
#else
GLFMViewController *viewController = (GLFMViewController *)self.rootViewController;
#endif
if (viewController.glfmDisplay && viewController.glfmDisplay->focusFunc) {
viewController.glfmDisplay->focusFunc(viewController.glfmDisplay, _active);
}
if (viewController.isViewLoaded) {
if (!active) {
// Draw once when entering the background so that a game can show "paused" state.
[viewController.glfmView requestRefresh];
[viewController.glfmView draw];
}
viewController.glfmView.animating = active;
}
#if TARGET_OS_IOS
if (viewController.isMotionManagerLoaded) {
[viewController updateMotionManagerActiveState];
}
#endif
#if TARGET_OS_IOS || TARGET_OS_TV
[viewController clearTouches];
#endif
#if TARGET_OS_OSX
[viewController clearActiveKeys];
#endif
}
}
@end
#if TARGET_OS_IOS || TARGET_OS_TV
// MARK: - UISceneDelegate (iOS, tvOS)
@interface GLFMSceneDelegate : NSObject<UISceneDelegate>
@property(nonatomic, strong) GLFMWindow *window;
@end
@implementation GLFMSceneDelegate
@synthesize window;
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session
options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0), tvos(13.0)) {
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
self.window = GLFM_AUTORELEASE([[GLFMWindow alloc] initWithWindowScene:windowScene]);
self.window.rootViewController = GLFM_AUTORELEASE([[GLFMViewController alloc] initWithDefaultFrame:self.window.bounds
contentScale:windowScene.screen.nativeScale]);
[self.window makeKeyAndVisible];
}
}
- (void)sceneDidDisconnect:(UIScene *)scene API_AVAILABLE(ios(13.0), tvos(13.0)) {
self.window.active = NO;
}
- (void)sceneDidBecomeActive:(UIScene *)scene API_AVAILABLE(ios(13.0), tvos(13.0)) {
self.window.active = YES;
}
- (void)sceneWillResignActive:(UIScene *)scene API_AVAILABLE(ios(13.0), tvos(13.0)) {
self.window.active = NO;
}
- (void)sceneWillEnterForeground:(UIScene *)scene API_AVAILABLE(ios(13.0), tvos(13.0)) {
self.window.active = YES;
}
- (void)sceneDidEnterBackground:(UIScene *)scene API_AVAILABLE(ios(13.0), tvos(13.0)) {
self.window.active = NO;
}
- (void)dealloc {
self.window = nil;
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
@end
// MARK: - GLFMAppDelegate (iOS, tvOS)
@interface GLFMAppDelegate : NSObject<UIApplicationDelegate>
@property(nonatomic, strong) GLFMWindow *window; // window for iOS 12 and older
@end
@implementation GLFMAppDelegate
@synthesize window;
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
#if 0 // I don't know why this doesn't work
if (@available(iOS 13, tvOS 13, *)) {
// Create the window in GLFMSceneDelegate
} else
#endif
{
self.window = GLFM_AUTORELEASE([[GLFMWindow alloc] init]);
if (self.window.bounds.size.width <= (CGFloat)0.0 || self.window.bounds.size.height <= (CGFloat)0.0) {
// Set UIWindow frame for iOS 8.
// On iOS 9, the UIWindow frame may be different than the UIScreen bounds for iPad's
// Split View or Slide Over.
self.window.frame = [[UIScreen mainScreen] bounds];
}
self.window.rootViewController = GLFM_AUTORELEASE([[GLFMViewController alloc] initWithDefaultFrame:self.window.bounds
contentScale:[UIScreen mainScreen].nativeScale]);
[self.window makeKeyAndVisible];
}
return YES;
}
- (UISceneConfiguration *)application:(UIApplication *)application
configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0), tvos(13.0)) {
UISceneConfiguration *sceneConfig = GLFM_AUTORELEASE([[UISceneConfiguration alloc] initWithName:@"GLFM Configuration"
sessionRole:connectingSceneSession.role]);
sceneConfig.delegateClass = [GLFMSceneDelegate class];
return sceneConfig;
}
- (void)applicationWillResignActive:(UIApplication *)application {
self.window.active = NO;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
self.window.active = NO;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
self.window.active = YES;
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
self.window.active = YES;
}
- (void)applicationWillTerminate:(UIApplication *)application {
self.window.active = NO;
}
- (void)dealloc {
self.window = nil;
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
@end
#endif // TARGET_OS_IOS || TARGET_OS_TV
#if TARGET_OS_OSX
// MARK: - GLFMAppDelegate (macOS)
@interface GLFMAppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate>
@property(nonatomic, strong) GLFMWindow *window;
@end
@implementation GLFMAppDelegate
@synthesize window;
- (void)createDefaultMenuWithAppName:(NSString *)appName {
NSMenu *appMenu = GLFM_AUTORELEASE([NSMenu new]);
NSApp.mainMenu = GLFM_AUTORELEASE([NSMenu new]);
NSApp.servicesMenu = GLFM_AUTORELEASE([NSMenu new]);
NSApp.windowsMenu = GLFM_AUTORELEASE([[NSMenu alloc] initWithTitle:NSLocalizedString(@"Window", nil)]);
// App Menu
[NSApp.mainMenu addItemWithTitle:@"" action:nil keyEquivalent:@""].submenu = appMenu;
[appMenu addItemWithTitle:[NSString stringWithFormat:NSLocalizedString(@"About %@", nil), appName]
action:@selector(orderFrontStandardAboutPanel:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:NSLocalizedString(@"Services", nil)
action:nil
keyEquivalent:@""].submenu = NSApp.servicesMenu;
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Hide %@", nil), appName]
action:@selector(hide:)
keyEquivalent:@"h"];
[appMenu addItemWithTitle:NSLocalizedString(@"Hide Others", nil)
action:@selector(hideOtherApplications:)
keyEquivalent:@"h"].keyEquivalentModifierMask = NSEventModifierFlagOption | NSEventModifierFlagCommand;
[appMenu addItemWithTitle:NSLocalizedString(@"Show All", nil)
action:@selector(unhideAllApplications:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:NSLocalizedString(@"Quit %@", nil), appName]
action:@selector(terminate:)
keyEquivalent:@"q"];
// Window Menu
[NSApp.mainMenu addItemWithTitle:@"" action:nil keyEquivalent:@""].submenu = NSApp.windowsMenu;
[NSApp.windowsMenu addItemWithTitle:NSLocalizedString(@"Minimize", nil)
action:@selector(performMiniaturize:)
keyEquivalent:@"m"];
[NSApp.windowsMenu addItemWithTitle:NSLocalizedString(@"Zoom", nil)
action:@selector(performZoom:)
keyEquivalent:@""];
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
// App name (used for menu bar and window title) is "CFBundleName" or the process name.
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *appName = infoDictionary[@"CFBundleName"];
if (!appName || appName.length == 0) {
appName = [[NSProcessInfo processInfo] processName];
}
// Window style: Closable, miniaturizable, resizable.
NSWindowStyleMask windowStyle = (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable);
// Create NSWindow centered on the main screen, One-half screen size.
double width, height, scale;
glfm__getDefaultDisplaySize(NULL, &width, &height, &scale);
CGRect screenFrame = [NSScreen mainScreen].frame;
CGRect contentFrame;
contentFrame.origin.x = screenFrame.origin.x + screenFrame.size.width / 2 - width / 2;
contentFrame.origin.y = screenFrame.origin.y + screenFrame.size.height / 2 - height / 2;
contentFrame.size.width = width;
contentFrame.size.height = height;
self.window = GLFM_AUTORELEASE([[GLFMWindow alloc] initWithContentRect:contentFrame
styleMask:windowStyle
backing:NSBackingStoreBuffered
defer:NO]);
self.window.title = appName;
//self.window.titlebarSeparatorStyle = NSTitlebarSeparatorStyleNone; // Make the topmost row of the view visible
self.window.excludedFromWindowsMenu = YES; // Single-window app
self.window.tabbingMode = NSWindowTabbingModeDisallowed; // No tabs
self.window.releasedWhenClosed = NO;
self.window.acceptsMouseMovedEvents = YES;
self.window.delegate = self;
CGRect defaultFrame = [self.window contentRectForFrameRect:self.window.frame];
defaultFrame.origin = CGPointZero;
GLFMViewController *glfmViewController = GLFM_AUTORELEASE([[GLFMViewController alloc] initWithDefaultFrame:defaultFrame
contentScale:(CGFloat)scale]);
// Set the contentViewController and call glfmMain() (in loadView).
self.window.contentViewController = glfmViewController;
if (!self.window.contentViewController.isViewLoaded) {
[self.window.contentViewController loadView];
}
// Check if window size should change (due to orientation setting)
glfm__getDefaultDisplaySize(glfmViewController.glfmDisplay, &width, &height, &scale);
if (!glfm__isCGFloatEqual(width, (double)contentFrame.size.width) ||
glfm__isCGFloatEqual(height, (double)contentFrame.size.height)) {
contentFrame.origin.x = screenFrame.origin.x + screenFrame.size.width / 2 - width / 2;
contentFrame.origin.y = screenFrame.origin.y + screenFrame.size.height / 2 - height / 2;
contentFrame.size.width = width;
contentFrame.size.height = height;
[self.window setFrame:[self.window frameRectForContentRect:contentFrame] display:NO];
}
// Create default menu if one wasn't created in glfmMain()
if (!NSApp.mainMenu) {
[self createDefaultMenuWithAppName:appName];
}
// Enter fullscreen if requested
glfm__displayChromeUpdated(glfmViewController.glfmDisplay);
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
// Draw first before showing the window
GLFMViewController *glfmViewController = (GLFMViewController *)self.window.contentViewController;
[glfmViewController.glfmViewIfLoaded draw];
if (NSApp.activationPolicy == NSApplicationActivationPolicyRegular) {
[self.window makeKeyWindow];
[self.window orderFrontRegardless];
} else {
// Executable-only (unbundled) app
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[self.window makeKeyWindow];
[self.window orderFrontRegardless];
[NSApp activateIgnoringOtherApps:YES];
}
}
- (void)applicationWillResignActive:(NSNotification *)notification {
self.window.active = NO;
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
self.window.active = !self.window.miniaturized;
}
- (void)windowWillMiniaturize:(NSNotification *)notification {
self.window.active = NO;
}
- (void)windowDidDeminiaturize:(NSNotification *)notification {
self.window.active = NSApp.active;
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
if (!self.window) {
return NSTerminateNow;
}
// Terminate later in windowWillClose:
[self.window close];
return NSTerminateCancel;
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
if (self.window == notification.object) {
GLFMViewController *glfmViewController = (GLFMViewController *)self.window.contentViewController;
self.window.styleMask |= NSWindowStyleMaskFullScreen; // Style isn't set before notification
glfm__displayChromeUpdated(glfmViewController.glfmDisplay);
}
}
- (void)windowWillExitFullScreen:(NSNotification *)notification {
if (self.window == notification.object) {
GLFMViewController *glfmViewController = (GLFMViewController *)self.window.contentViewController;
self.window.styleMask &= ~NSWindowStyleMaskFullScreen; // Style isn't set before notification
glfm__displayChromeUpdated(glfmViewController.glfmDisplay);
}
}
- (void)windowWillClose:(NSNotification *)notification {
if (self.window == notification.object) {
self.window.active = NO;
self.window = nil;
// Dispatch later, after surfaceDestroyedFunc is called
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp terminate:nil];
});
}
}
- (void)dealloc {
self.window = nil;
#if !__has_feature(objc_arc)
[super dealloc];
#endif
}
@end
#endif // TARGET_OS_OSX
// MARK: - Main
#if TARGET_OS_IOS || TARGET_OS_TV
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([GLFMAppDelegate class]));
}
}
#else
int main(int argc, const char * argv[]) {
(void)argc;
(void)argv;
@autoreleasepool {
// Create the sharedApplication instance from "NSPrincipalClass"
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *principalClassName = infoDictionary[@"NSPrincipalClass"];
Class principalClass = NSClassFromString(principalClassName);
if ([principalClass respondsToSelector:@selector(sharedApplication)]) {
[principalClass sharedApplication];
} else {
[NSApplication sharedApplication];
}
// Set the delegate and run
GLFMAppDelegate *delegate = [GLFMAppDelegate new];
[NSApp setDelegate:delegate];
[NSApp run];
GLFM_RELEASE(delegate);
}
return 0;
}
#endif // TARGET_OS_OSX
// MARK: - GLFM private functions
static void glfm__getDefaultDisplaySize(const GLFMDisplay *display,
double *width, double *height, double *scale) {
#if TARGET_OS_IOS || TARGET_OS_TV
(void)display;
CGSize size = UIScreen.mainScreen.bounds.size;
*width = (double)size.width;
*height = (double)size.height;
*scale = UIScreen.mainScreen.nativeScale;
#else
NSScreen *screen = [NSScreen mainScreen];
*scale = screen.backingScaleFactor;
// Create a window half the size of the screen.
// If portrait orientation (but not landscape) is specified, use a portrait-sized default window.
GLFMInterfaceOrientation supportedOrientations = !display ? GLFMInterfaceOrientationUnknown : display->supportedOrientations;
if (((supportedOrientations & GLFMInterfaceOrientationPortrait) != 0 ||
(supportedOrientations & GLFMInterfaceOrientationPortraitUpsideDown) != 0) &&
(supportedOrientations & GLFMInterfaceOrientationLandscapeLeft) == 0 &&
(supportedOrientations & GLFMInterfaceOrientationLandscapeRight) == 0) {
*width = screen.frame.size.height / 2;
*height = screen.frame.size.width / 2;
} else {
*width = screen.frame.size.width / 2;
*height = screen.frame.size.height / 2;
}
#endif
}
/// Get drawable size in pixels from display dimensions in points.
static void glfm__getDrawableSize(double displayWidth, double displayHeight, double displayScale,
int *width, int *height) {
int newDrawableWidth = (int)(displayWidth * displayScale);
int newDrawableHeight = (int)(displayHeight * displayScale);
#if TARGET_OS_IOS
// On the iPhone 6 when "Display Zoom" is set, the size will be incorrect.
if (glfm__isCGFloatEqual(displayScale, (CGFloat)2.343750)) {
if (newDrawableWidth == 750 && newDrawableHeight == 1331) {
newDrawableHeight = 1334;
} else if (newDrawableWidth == 1331 && newDrawableHeight == 750) {
newDrawableWidth = 1334;
}
}
#endif
if (width) *width = newDrawableWidth;
if (height) *height = newDrawableHeight;
}
static void glfm__displayChromeUpdated(GLFMDisplay *display) {
if (display && display->platformData) {
#if TARGET_OS_IOS
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
[viewController.glfmViewIfLoaded requestRefresh];
[viewController setNeedsStatusBarAppearanceUpdate];
if (@available(iOS 11, *)) {
[viewController setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
#elif TARGET_OS_OSX
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
NSWindow *window = viewController.glfmViewIfLoaded.window;
if (window) {
// This might not make sense and will probably change.
// * GLFMUserInterfaceChromeNavigation: Full content view (title bar overlays content).
// * GLFMUserInterfaceChromeNavigationAndStatusBar: Title bar outside content view (no overlay).
// * GLFMUserInterfaceChromeNone: No window decor, but user can drag title bar area to move the window.
// * GLFMUserInterfaceChromeNone when window is fullscreen: No menu bar.
// Prevent title bar buttons from being tapped when their alpha is zero
BOOL hideButtons = ((window.styleMask & NSWindowStyleMaskFullScreen) == 0 &&
display->uiChrome == GLFMUserInterfaceChromeNone);
NSView *titleBarView = [window standardWindowButton:NSWindowCloseButton].superview;
for (NSView *button in titleBarView.subviews) {
if ([button isKindOfClass:[NSControl class]]) {
[button setHidden:hideButtons];
}
}
// Hide/Show title bar
if (@available(macOS 11, *)) {
titleBarView = titleBarView.superview;
}
if (window.styleMask & NSWindowStyleMaskFullScreen) {
titleBarView.alphaValue = (CGFloat)1.0;
[NSMenu setMenuBarVisible:display->uiChrome != GLFMUserInterfaceChromeNone];
} else {
[NSMenu setMenuBarVisible:YES];
if (display->uiChrome == GLFMUserInterfaceChromeNone) {
if ((window.styleMask & NSWindowStyleMaskFullSizeContentView) == 0) {
titleBarView.alphaValue = (CGFloat)0.0;
} else {
[[titleBarView animator] setAlphaValue:(CGFloat)0.0];
}
} else {
[[titleBarView animator] setAlphaValue:(CGFloat)1.0];
}
// Enable/disable full-size content view (under the title bar)
if (display->uiChrome == GLFMUserInterfaceChromeNavigationAndStatusBar) {
window.styleMask &= ~NSWindowStyleMaskFullSizeContentView;
} else {
window.styleMask |= NSWindowStyleMaskFullSizeContentView;
}
}
}
#endif
}
}
static void glfm__sensorFuncUpdated(GLFMDisplay *display) {
#if TARGET_OS_IOS
if (display) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
[viewController updateMotionManagerActiveState];
}
#else
(void)display;
#endif
}
// MARK: - GLFM public functions
double glfmGetTime(void) {
return CACurrentMediaTime();
}
GLFMProc glfmGetProcAddress(const char *functionName) {
static void *handle = NULL;
if (!handle) {
handle = dlopen(NULL, RTLD_LAZY);
}
return handle ? (GLFMProc)dlsym(handle, functionName) : NULL;
}
void glfmSwapBuffers(GLFMDisplay *display) {
if (display && display->platformData) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
[viewController.glfmViewIfLoaded swapBuffers];
}
}
void glfmSetSupportedInterfaceOrientation(GLFMDisplay *display, GLFMInterfaceOrientation supportedOrientations) {
if (display) {
if (display->supportedOrientations != supportedOrientations) {
display->supportedOrientations = supportedOrientations;
#if TARGET_OS_IOS
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
if (@available(iOS 16, *)) {
[viewController setNeedsUpdateOfSupportedInterfaceOrientations];
} else if (viewController.isViewLoaded && viewController.view.window) {
// HACK: Notify that the value of supportedInterfaceOrientations has changed
[viewController.glfmView requestRefresh];
UIViewController *tempViewController = GLFM_AUTORELEASE([[UIViewController alloc] init]);
tempViewController.view = GLFM_AUTORELEASE([[UIView alloc] init]);
[viewController presentViewController:tempViewController animated:NO completion:^{
[viewController dismissViewControllerAnimated:NO completion:NULL];
}];
}
#endif
}
}
}
GLFMInterfaceOrientation glfmGetInterfaceOrientation(const GLFMDisplay *display) {
(void)display;
#if TARGET_OS_IOS
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
switch (orientation) {
case UIInterfaceOrientationPortrait:
return GLFMInterfaceOrientationPortrait;
case UIInterfaceOrientationPortraitUpsideDown:
return GLFMInterfaceOrientationPortraitUpsideDown;
case UIInterfaceOrientationLandscapeLeft:
return GLFMInterfaceOrientationLandscapeLeft;
case UIInterfaceOrientationLandscapeRight:
return GLFMInterfaceOrientationLandscapeRight;
case UIInterfaceOrientationUnknown: default:
return GLFMInterfaceOrientationUnknown;
}
#else
return GLFMInterfaceOrientationUnknown;
#endif
}
void glfmGetDisplaySize(const GLFMDisplay *display, int *width, int *height) {
if (display && display->platformData) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
if (viewController.isViewLoaded) {
if (width) *width = viewController.glfmView.drawableWidth;
if (height) *height = viewController.glfmView.drawableHeight;
} else {
double displayWidth, displayHeight, displayScale;
glfm__getDefaultDisplaySize(display, &displayWidth, &displayHeight, &displayScale);
glfm__getDrawableSize(displayWidth, displayHeight, displayScale, width, height);
}
} else {
if (width) *width = 0;
if (height) *height = 0;
}
}
double glfmGetDisplayScale(const GLFMDisplay *display) {
#if TARGET_OS_OSX
NSWindow *window = nil;
if (display && display->platformData) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
window = viewController.glfmViewIfLoaded.window;
}
return window ? window.backingScaleFactor : [NSScreen mainScreen].backingScaleFactor;
#else
(void)display;
return (double)[UIScreen mainScreen].nativeScale;
#endif
}
void glfmGetDisplayChromeInsets(const GLFMDisplay *display, double *top, double *right,
double *bottom, double *left) {
if (display && display->platformData) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
if (!viewController.isViewLoaded) {
if (top) *top = 0.0;
if (right) *right = 0.0;
if (bottom) *bottom = 0.0;
if (left) *left = 0.0;
} else if (@available(iOS 11, tvOS 11, macOS 11, *)) {
#if TARGET_OS_IOS || TARGET_OS_TV
UIEdgeInsets insets = viewController.view.safeAreaInsets;
if (top) *top = (double)(insets.top * viewController.view.contentScaleFactor);
if (right) *right = (double)(insets.right * viewController.view.contentScaleFactor);
if (bottom) *bottom = (double)(insets.bottom * viewController.view.contentScaleFactor);
if (left) *left = (double)(insets.left * viewController.view.contentScaleFactor);
#else
// NOTE: This has not been tested.
// Run glfm_test_pattern fullscreen on a 2021-2022 MacBook Pro/Air with a notch.
NSEdgeInsets insets = viewController.view.safeAreaInsets;
if (top) *top = (double)(insets.top * viewController.view.layer.contentsScale);
if (right) *right = (double)(insets.right * viewController.view.layer.contentsScale);
if (bottom) *bottom = (double)(insets.bottom * viewController.view.layer.contentsScale);
if (left) *left = (double)(insets.left * viewController.view.layer.contentsScale);
#endif
} else {
if (top) {
#if TARGET_OS_IOS
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if (![viewController prefersStatusBarHidden]) {
*top = (double)([UIApplication sharedApplication].statusBarFrame.size.height *
viewController.view.contentScaleFactor);
} else {
*top = 0.0;
}
#pragma clang diagnostic pop
#else
*top = 0.0;
#endif
}
if (right) *right = 0.0;
if (bottom) *bottom = 0.0;
if (left) *left = 0.0;
}
} else {
if (top) *top = 0.0;
if (right) *right = 0.0;
if (bottom) *bottom = 0.0;
if (left) *left = 0.0;
}
}
GLFMRenderingAPI glfmGetRenderingAPI(const GLFMDisplay *display) {
if (!display || !display->platformData) {
return GLFMRenderingAPIOpenGLES2;
}
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
if (viewController.isViewLoaded) {
return viewController.glfmView.renderingAPI;
}
return GLFMRenderingAPIOpenGLES2;
}
bool glfmHasTouch(const GLFMDisplay *display) {
(void)display;
#if TARGET_OS_IOS || TARGET_OS_TV
return true;
#else
return false;
#endif
}
void glfmSetMouseCursor(GLFMDisplay *display, GLFMMouseCursor mouseCursor) {
#if TARGET_OS_OSX
if (display && display->platformData) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
NSCursor *cursor;
switch (mouseCursor) {
case GLFMMouseCursorAuto:
case GLFMMouseCursorDefault:
default:
cursor = NSCursor.arrowCursor;
break;
case GLFMMouseCursorPointer:
cursor = NSCursor.pointingHandCursor;
break;
case GLFMMouseCursorCrosshair:
cursor = NSCursor.crosshairCursor;
break;
case GLFMMouseCursorText:
cursor = NSCursor.IBeamCursor;
break;
case GLFMMouseCursorVerticalText:
cursor = NSCursor.IBeamCursorForVerticalLayout;
break;
case GLFMMouseCursorNone:
cursor = viewController.transparentCursor;
break;
}
if (viewController.currentCursor != cursor) {
viewController.currentCursor = cursor;
GLFMWindow *window = (GLFMWindow *)viewController.glfmViewIfLoaded.window;
if (viewController.mouseInside && window.active) {
[cursor set];
}
}
[NSCursor setHiddenUntilMouseMoves:NO];
viewController.hideMouseCursorWhileTyping = (mouseCursor == GLFMMouseCursorAuto ||
mouseCursor == GLFMMouseCursorText ||
mouseCursor == GLFMMouseCursorVerticalText);
}
#elif TARGET_OS_IOS
if (@available(iOS 13.4, *)) {
if (display && display->platformData) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
if (viewController.mouseCursor != mouseCursor) {
viewController.mouseCursor = mouseCursor;
NSArray<id<UIInteraction>> *interactions = viewController.viewIfLoaded.interactions;
if (interactions) {
for (id<UIInteraction> interaction in interactions) {
if ([interaction isKindOfClass:[UIPointerInteraction class]]) {
UIPointerInteraction *pointerInteraction = (UIPointerInteraction *)interaction;
[pointerInteraction invalidate];
}
}
}
}
}
}
#else
(void)display;
(void)mouseCursor;
// Do nothing
#endif
}
void glfmSetMultitouchEnabled(GLFMDisplay *display, bool multitouchEnabled) {
#if TARGET_OS_IOS
if (display) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
viewController.multipleTouchEnabled = (BOOL)multitouchEnabled;
viewController.glfmViewIfLoaded.multipleTouchEnabled = (BOOL)multitouchEnabled;
}
#else
(void)display;
(void)multitouchEnabled;
#endif
}
bool glfmGetMultitouchEnabled(const GLFMDisplay *display) {
#if TARGET_OS_IOS
if (!display) {
return false;
}
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
return viewController.multipleTouchEnabled;
#else
(void)display;
return false;
#endif
}
bool glfmHasVirtualKeyboard(const GLFMDisplay *display) {
(void)display;
#if TARGET_OS_IOS
return true;
#else
return false;
#endif
}
void glfmSetKeyboardVisible(GLFMDisplay *display, bool visible) {
#if TARGET_OS_IOS
if (display) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
[viewController resignFirstResponder];
viewController.keyboardRequested = visible;
[viewController becomeFirstResponder];
}
#else
(void)display;
(void)visible;
#endif
}
bool glfmIsKeyboardVisible(const GLFMDisplay *display) {
#if TARGET_OS_IOS
if (!display) {
return false;
}
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
return viewController.keyboardRequested;
#else
(void)display;
return false;
#endif
}
bool glfmIsSensorAvailable(const GLFMDisplay *display, GLFMSensor sensor) {
#if TARGET_OS_IOS
if (display) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
switch (sensor) {
case GLFMSensorAccelerometer:
return viewController.motionManager.deviceMotionAvailable && viewController.motionManager.accelerometerAvailable;
case GLFMSensorMagnetometer:
return viewController.motionManager.deviceMotionAvailable && viewController.motionManager.magnetometerAvailable;
case GLFMSensorGyroscope:
return viewController.motionManager.deviceMotionAvailable && viewController.motionManager.gyroAvailable;
case GLFMSensorRotationMatrix:
return (viewController.motionManager.deviceMotionAvailable &&
([CMMotionManager availableAttitudeReferenceFrames] & CMAttitudeReferenceFrameXMagneticNorthZVertical));
}
}
return false;
#else
(void)display;
(void)sensor;
return false;
#endif
}
bool glfmIsHapticFeedbackSupported(const GLFMDisplay *display) {
(void)display;
#if TARGET_OS_IOS
if (@available(iOS 13, *)) {
return [CHHapticEngine capabilitiesForHardware].supportsHaptics;
}
#endif
return false;
}
void glfmPerformHapticFeedback(GLFMDisplay *display, GLFMHapticFeedbackStyle style) {
(void)display;
#if TARGET_OS_IOS
if (@available(iOS 10, *)) {
UIImpactFeedbackStyle uiStyle;
switch (style) {
case GLFMHapticFeedbackLight: default:
uiStyle = UIImpactFeedbackStyleLight;
break;
case GLFMHapticFeedbackMedium:
uiStyle = UIImpactFeedbackStyleMedium;
break;
case GLFMHapticFeedbackHeavy:
uiStyle = UIImpactFeedbackStyleHeavy;
break;
}
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:uiStyle];
[generator impactOccurred];
GLFM_RELEASE(generator);
}
#else
(void)style;
#endif
}
#if TARGET_OS_TV
bool glfmHasClipboardText(const GLFMDisplay *display) {
(void)display;
return false;
}
void glfmRequestClipboardText(GLFMDisplay *display, GLFMClipboardTextFunc clipboardTextFunc) {
(void)display;
if (clipboardTextFunc) {
clipboardTextFunc(display, NULL);
}
}
bool glfmSetClipboardText(GLFMDisplay *display, const char *string) {
(void)display;
(void)string;
return false;
}
#else
bool glfmHasClipboardText(const GLFMDisplay *display) {
(void)display;
#if TARGET_OS_OSX
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
return [pasteboard.types containsObject:NSPasteboardTypeString];
#else
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
return [pasteboard hasStrings];
#endif
}
void glfmRequestClipboardText(GLFMDisplay *display, GLFMClipboardTextFunc clipboardTextFunc) {
if (!clipboardTextFunc) {
return;
}
// Run in a background thread because the text may be retrieved from the network (iCloud clipboard)
// or show a confirmation dialog on iOS.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *text = nil;
if (glfmHasClipboardText(display)) {
#if TARGET_OS_OSX
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
text = [pasteboard stringForType:NSPasteboardTypeString];
#else
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
text = pasteboard.string;
#endif
}
dispatch_async(dispatch_get_main_queue(), ^{
clipboardTextFunc(display, text.UTF8String);
});
});
}
bool glfmSetClipboardText(GLFMDisplay *display, const char *string) {
(void)display;
@autoreleasepool {
NSString *text = string ? [NSString stringWithUTF8String:string] : nil;
if (!text) {
return false;
}
#if TARGET_OS_OSX
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil];
return ([pasteboard setString:text forType:NSPasteboardTypeString] == YES);
#else
[UIPasteboard generalPasteboard].string = text;
return true;
#endif
}
}
#endif // !TARGET_OS_TV
// MARK: - Apple-specific functions
bool glfmIsMetalSupported(const GLFMDisplay *display) {
#if GLFM_INCLUDE_METAL
if (display) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
return (viewController.metalDevice != nil);
}
#endif
return false;
}
void *glfmGetMetalView(const GLFMDisplay *display) {
#if GLFM_INCLUDE_METAL
if (display) {
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
UIView<GLFMView> *view = viewController.glfmViewIfLoaded;
if ([view isKindOfClass:[MTKView class]]) {
return (__bridge void *)view;
}
}
#endif
return NULL;
}
void *glfmGetViewController(const GLFMDisplay *display) {
if (!display) {
return NULL;
}
GLFMViewController *viewController = (__bridge GLFMViewController *)display->platformData;
return (__bridge void *)viewController;
}
#endif // __APPLE__