Files
factorio-ios/platform/native/shim/appkit.m
T
SashaGoncharov19 51d085e865 Native iPhone port of Factorio 2.0.77
Run the original macOS ARM64 game code on iOS:
- tools/retarget.py: header-only Mach-O retargeting to an iOS dylib
- platform/native: UIKit host and macOS API shims (AppKit for SDL's
  Cocoa backend, CoreGraphics displays, CoreAudio device, Metal device
  properties, Wuffs PNG decoding, Steam and peripheral stubs)
- touch, overlay, iOS keyboard and hardware keyboard input
- tools/build_native.py: build, sign, install, data copy, launch
- docs: build guide, controls, architecture

Also includes the earlier static-translation research (ARM64 -> WASM ->
native) under docs/research, runtime/, tests/ and tools/.

No game files are included; the build reads them from a local install.
2026-10-23 15:27:58 +04:00

922 lines
43 KiB
Objective-C

// AppKit subset implemented on UIKit for the retargeted macOS binary.
// Scope: what Factorio's statically linked SDL 2.24 Cocoa backend needs to create one
// fullscreen window with a CAMetalLayer and receive mouse-style input from touches.
// Unknown selectors on shim classes are logged once and return zero.
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
#import <ImageIO/ImageIO.h>
#import <Accelerate/Accelerate.h>
#import "fx.h"
#import "pngdecode.h"
NSUInteger fx_current_modifier_flags(void);
// ---- Exported data symbols -------------------------------------------------------------
__attribute__((visibility("default"))) id NSApp = nil;
__attribute__((visibility("default"))) const double NSAppKitVersionNumber = 2022.0;
#define FX_STRING(name) __attribute__((visibility("default"))) NSString *const name = @#name;
FX_STRING(NSApplicationDidBecomeActiveNotification)
FX_STRING(NSBackingPropertyOldScaleFactorKey)
FX_STRING(NSDeviceRGBColorSpace)
FX_STRING(NSFilenamesPboardType)
FX_STRING(NSWindowDidBecomeKeyNotification)
FX_STRING(NSWindowDidChangeBackingPropertiesNotification)
FX_STRING(NSWindowDidChangeScreenNotification)
FX_STRING(NSWindowDidChangeScreenProfileNotification)
FX_STRING(NSWindowDidDeminiaturizeNotification)
FX_STRING(NSWindowDidEnterFullScreenNotification)
FX_STRING(NSWindowDidExitFullScreenNotification)
FX_STRING(NSWindowDidExposeNotification)
FX_STRING(NSWindowDidMiniaturizeNotification)
FX_STRING(NSWindowDidMoveNotification)
FX_STRING(NSWindowDidResignKeyNotification)
FX_STRING(NSWindowDidResizeNotification)
FX_STRING(NSWindowWillCloseNotification)
FX_STRING(NSWindowWillEnterFullScreenNotification)
FX_STRING(NSWindowWillExitFullScreenNotification)
__attribute__((visibility("default"))) NSString *const NSPasteboardTypeString = @"public.utf8-plain-text";
__attribute__((visibility("default"))) void NSRectFill(CGRect rect) { (void)rect; }
// AppKit event constants (values from the macOS SDK).
enum {
FXLeftMouseDown = 1, FXLeftMouseUp = 2, FXRightMouseDown = 3, FXRightMouseUp = 4,
FXMouseMoved = 5, FXLeftMouseDragged = 6, FXRightMouseDragged = 7,
FXKeyDown = 10, FXKeyUp = 11, FXFlagsChanged = 12, FXScrollWheel = 22,
FXOtherMouseDown = 25, FXOtherMouseUp = 26, FXOtherMouseDragged = 27,
};
// ---- Unknown-selector fallback ----------------------------------------------------------
static id fx_zero_imp(id self, SEL _cmd, ...) { return nil; }
@interface FXShimObject : NSObject
@end
@implementation FXShimObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
const char *name = sel_getName(sel);
if (name[0] == '_' || name[0] == '.' || strncmp(name, "init", 4) == 0) return NO;
fx_log("[fx] unimplemented -[%s %s] -> 0", class_getName(self), name);
class_addMethod(self, sel, (IMP)fx_zero_imp, "@@:");
return YES;
}
+ (BOOL)resolveClassMethod:(SEL)sel {
const char *name = sel_getName(sel);
if (name[0] == '_' || name[0] == '.') return NO;
// KVO probes these optional class methods; answering them would disable automatic
// change notifications (SDL observes -[NSWindow visible]).
if (!strncmp(name, "automaticallyNotifiesObserversOf", 32) || !strncmp(name, "keyPathsForValuesAffecting", 26)) return NO;
fx_log("[fx] unimplemented +[%s %s] -> 0", class_getName(self), name);
class_addMethod(object_getClass(self), sel, (IMP)fx_zero_imp, "@@:");
return YES;
}
@end
// ---- Screen geometry --------------------------------------------------------------------
// The game sees an AppKit screen of "game points" with backing scale 1. By default one game
// point is one physical pixel, so Factorio renders at the panel's native resolution (its own UI
// scale setting then controls legibility). Documents/factorio/render-divisor.txt can hold a
// divisor (e.g. 1.5) to render fewer pixels. UIKit views and touches convert via fx_game_per_uikit().
static CGSize fx_landscape(CGSize s) { return s.width >= s.height ? s : CGSizeMake(s.height, s.width); }
static CGSize fx_uikit_points(void) { return fx_landscape(UIScreen.mainScreen.bounds.size); }
FX_EXPORT double fx_render_divisor(void) {
static double divisor;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *text = [NSString stringWithContentsOfFile:[docs stringByAppendingPathComponent:@"factorio/render-divisor.txt"]
encoding:NSUTF8StringEncoding error:nil];
divisor = text ? text.doubleValue : 0;
if (divisor < 1 || divisor > 4) divisor = 1;
});
return divisor;
}
CGSize fx_screen_points(void) {
CGSize px = fx_landscape(UIScreen.mainScreen.nativeBounds.size);
double d = fx_render_divisor();
return CGSizeMake(floor(px.width / d), floor(px.height / d));
}
double fx_screen_scale(void) { return 1.0; }
FX_EXPORT double fx_game_per_uikit(void) { return fx_screen_points().width / fx_uikit_points().width; }
// ---- NSScreen ---------------------------------------------------------------------------
@interface NSScreen : FXShimObject
@end
@implementation NSScreen
+ (NSArray *)screens { static NSArray *s; static dispatch_once_t once; dispatch_once(&once, ^{ s = @[[NSScreen new]]; }); return s; }
+ (NSScreen *)mainScreen { return self.screens[0]; }
+ (NSScreen *)deepestScreen { return self.screens[0]; }
- (CGRect)frame { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
- (CGRect)visibleFrame { return self.frame; }
- (CGFloat)backingScaleFactor { return fx_screen_scale(); }
- (NSDictionary *)deviceDescription { return @{@"NSScreenNumber": @(FX_DISPLAY_ID)}; }
- (NSString *)localizedName { return @"iPhone"; }
- (NSInteger)maximumFramesPerSecond { return UIScreen.mainScreen.maximumFramesPerSecond; }
- (id)colorSpace { return nil; }
- (CGRect)convertRectToBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x*k, r.origin.y*k, r.size.width*k, r.size.height*k); }
@end
// ---- NSEvent ----------------------------------------------------------------------------
static CGPoint fx_mouse_screen; // AppKit screen coordinates (bottom-left origin, points)
static NSUInteger fx_mouse_buttons; // bitmask of pressed buttons
@class NSWindow;
@interface NSEvent : FXShimObject
@property NSUInteger type;
@property CGPoint locationInWindow;
@property (weak) NSWindow *window;
@property NSUInteger modifierFlags;
@property NSInteger buttonNumber;
@property NSInteger clickCount;
@property CGFloat deltaX, deltaY, scrollingDeltaX, scrollingDeltaY;
@property NSTimeInterval timestamp;
@property unsigned short keyCode;
@property (copy) NSString *characters;
@property NSInteger subtype;
@end
@implementation NSEvent
+ (CGPoint)mouseLocation { return fx_mouse_screen; }
+ (NSUInteger)pressedMouseButtons { return fx_mouse_buttons; }
+ (NSUInteger)modifierFlags { return fx_current_modifier_flags(); }
+ (NSTimeInterval)doubleClickInterval { return 0.3; }
+ (instancetype)otherEventWithType:(NSUInteger)type location:(CGPoint)location modifierFlags:(NSUInteger)flags
timestamp:(NSTimeInterval)time windowNumber:(NSInteger)wn context:(id)ctx
subtype:(short)subtype data1:(NSInteger)d1 data2:(NSInteger)d2 {
NSEvent *e = [NSEvent new]; e.type = type; e.locationInWindow = location; e.modifierFlags = flags;
e.timestamp = time; e.subtype = subtype; return e;
}
- (BOOL)hasPreciseScrollingDeltas { return YES; }
- (BOOL)isDirectionInvertedFromDevice { return NO; }
- (BOOL)isARepeat { return NO; }
- (NSString *)charactersIgnoringModifiers { return self.characters; }
- (NSUInteger)phase { return 0; }
- (NSUInteger)momentumPhase { return 0; }
- (NSSet *)touchesMatchingPhase:(NSUInteger)phase inView:(id)view { return [NSSet set]; }
- (NSInteger)windowNumber { return 1; }
@end
// ---- NSResponder ------------------------------------------------------------------------
@interface NSResponder : FXShimObject
@property (strong) NSResponder *nextResponder;
@end
@implementation NSResponder
#define FX_FORWARD(sel) - (void)sel:(NSEvent *)e { [self.nextResponder sel:e]; }
FX_FORWARD(mouseDown) FX_FORWARD(mouseUp) FX_FORWARD(mouseDragged) FX_FORWARD(mouseMoved)
FX_FORWARD(rightMouseDown) FX_FORWARD(rightMouseUp) FX_FORWARD(rightMouseDragged)
FX_FORWARD(otherMouseDown) FX_FORWARD(otherMouseUp) FX_FORWARD(otherMouseDragged)
FX_FORWARD(scrollWheel) FX_FORWARD(keyDown) FX_FORWARD(keyUp) FX_FORWARD(flagsChanged)
FX_FORWARD(mouseEntered) FX_FORWARD(mouseExited)
FX_FORWARD(touchesBeganWithEvent) FX_FORWARD(touchesMovedWithEvent)
FX_FORWARD(touchesEndedWithEvent) FX_FORWARD(touchesCancelledWithEvent)
- (BOOL)acceptsFirstResponder { return YES; }
- (BOOL)becomeFirstResponder { return YES; }
- (BOOL)resignFirstResponder { return YES; }
// AppKit's text system, reduced to what SDL's field editor needs: printable characters become
// insertText:replacementRange:, everything else is a command (ignored by SDL).
- (void)interpretKeyEvents:(NSArray *)events {
for (NSEvent *e in events) {
NSString *c = e.characters;
unichar first = c.length ? [c characterAtIndex:0] : 0;
if (first >= 0x20 && first != 0x7f && !(first >= 0xF700 && first <= 0xF8FF)) {
if ([self respondsToSelector:@selector(insertText:replacementRange:)])
[(id)self insertText:c replacementRange:NSMakeRange(NSNotFound, 0)];
}
}
}
- (void)doCommandBySelector:(SEL)sel { }
- (BOOL)validateMenuItem:(id)item { return YES; }
- (id)undoManager { return nil; }
@end
// ---- NSView -----------------------------------------------------------------------------
@interface FXBackingView : UIView
@property (weak) id appkitView;
@end
@interface NSView : NSResponder
@property (nonatomic) CGRect frame;
@property (nonatomic, weak) NSView *superview;
@property (nonatomic, strong) NSMutableArray *subviewList;
@property (nonatomic, weak) NSWindow *window;
@property (nonatomic, strong) FXBackingView *backing;
@property (nonatomic, strong) CALayer *layer;
@property (nonatomic) BOOL wantsLayer;
@property (nonatomic) NSUInteger autoresizingMask;
@property (nonatomic, getter=isHidden) BOOL hidden;
- (void)fx_layout;
@end
@interface NSWindow : NSResponder
@property (nonatomic, strong) NSView *contentView;
@property (nonatomic, weak) id delegate;
@property (nonatomic, getter=isVisible) BOOL visible;
@property (nonatomic, getter=isKeyWindow) BOOL keyWindow;
@property (nonatomic) NSUInteger styleMask;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSResponder *firstResponder;
@property (nonatomic) BOOL releasedWhenClosed, oneShot, acceptsMouseMovedEvents, opaque, movableByWindowBackground;
@property (nonatomic) NSInteger level;
@property (nonatomic) NSUInteger collectionBehavior;
@property (nonatomic) NSInteger tabbingMode;
@property (nonatomic) CGFloat alphaValue;
@end
UIView *fx_root_view(void);
@implementation FXBackingView
- (void)layoutSubviews {
[super layoutSubviews];
for (CALayer *l in self.layer.sublayers) if ([l isKindOfClass:CAMetalLayer.class]) l.frame = self.bounds;
}
@end
@implementation NSView
- (instancetype)initWithFrame:(CGRect)frame {
if ((self = [super init])) {
_subviewList = [NSMutableArray array];
_frame = frame;
fx_main_sync(^{
self->_backing = [[FXBackingView alloc] initWithFrame:CGRectZero];
self->_backing.appkitView = self;
self->_backing.userInteractionEnabled = NO;
self->_backing.backgroundColor = UIColor.clearColor;
});
}
return self;
}
- (instancetype)init { return [self initWithFrame:CGRectZero]; }
- (CGRect)bounds { return CGRectMake(0, 0, _frame.size.width, _frame.size.height); }
- (void)setFrame:(CGRect)frame { _frame = frame; [self fx_layout]; for (NSView *v in _subviewList) [v fx_layout]; }
- (void)setFrameSize:(CGSize)size { self.frame = CGRectMake(_frame.origin.x, _frame.origin.y, size.width, size.height); }
- (void)setFrameOrigin:(CGPoint)origin { self.frame = CGRectMake(origin.x, origin.y, _frame.size.width, _frame.size.height); }
- (void)fx_layout {
// AppKit frames are bottom-left relative to the superview; UIKit is top-left.
CGFloat parentHeight = _superview ? _superview.frame.size.height : _frame.size.height;
CGFloat k = fx_game_per_uikit(); // game points -> UIKit points
CGRect ui = CGRectMake(_frame.origin.x / k, (parentHeight - _frame.origin.y - _frame.size.height) / k,
_frame.size.width / k, _frame.size.height / k);
fx_main_sync(^{ self->_backing.frame = ui; [self->_backing setNeedsLayout]; [self->_backing layoutIfNeeded]; });
}
- (NSArray *)subviews { return [_subviewList copy]; }
- (void)addSubview:(NSView *)view {
[view removeFromSuperview];
[_subviewList addObject:view];
view.superview = self;
view.window = self.window;
fx_main_sync(^{ [self->_backing addSubview:view.backing]; });
[view fx_layout];
}
- (void)removeFromSuperview {
NSView *parent = _superview;
if (!parent) return;
[parent.subviewList removeObject:self];
_superview = nil;
fx_main_sync(^{ [self->_backing removeFromSuperview]; });
}
- (void)setWindow:(NSWindow *)window { _window = window; for (NSView *v in _subviewList) v.window = window; }
- (id)viewWithTag:(NSInteger)tag {
if ([self tag] == tag) return self;
for (NSView *v in _subviewList) { id found = [v viewWithTag:tag]; if (found) return found; }
return nil;
}
- (NSInteger)tag { return -1; }
- (CALayer *)makeBackingLayer { return [CALayer layer]; }
- (BOOL)wantsUpdateLayer { return NO; }
- (void)updateLayer { }
- (void)setWantsLayer:(BOOL)wants {
_wantsLayer = wants;
if (wants && !_layer) {
__block CALayer *layer;
fx_main_sync(^{
layer = [self makeBackingLayer];
layer.frame = self->_backing.bounds;
[self->_backing.layer addSublayer:layer];
});
_layer = layer;
fx_log("[fx] -[%s setWantsLayer:YES] -> %s", class_getName(self.class), class_getName(layer.class));
}
}
- (void)setNeedsDisplay:(BOOL)flag { }
- (void)setNeedsLayout:(BOOL)flag { }
- (void)display { }
- (void)drawRect:(CGRect)rect { }
- (void)setHidden:(BOOL)hidden { _hidden = hidden; fx_main_sync(^{ self->_backing.hidden = hidden; }); }
- (void)setWantsBestResolutionOpenGLSurface:(BOOL)flag { }
- (void)setAcceptsTouchEvents:(BOOL)flag { }
- (void)setAllowedTouchTypes:(NSUInteger)types { }
- (void)registerForDraggedTypes:(NSArray *)types { }
- (void)unregisterDraggedTypes { }
- (void)addCursorRect:(CGRect)rect cursor:(id)cursor { }
- (void)discardCursorRects { }
- (void)resetCursorRects { }
- (void)lockFocus { }
- (void)unlockFocus { }
- (id)hitTest:(CGPoint)point { return self; }
- (BOOL)mouse:(CGPoint)p inRect:(CGRect)r { return CGRectContainsPoint(r, p); }
- (CGPoint)convertPoint:(CGPoint)p fromView:(NSView *)view { return p; }
- (CGPoint)convertPoint:(CGPoint)p toView:(NSView *)view { return p; }
- (CGRect)convertRect:(CGRect)r toView:(NSView *)view { return r; }
- (CGRect)convertRect:(CGRect)r fromView:(NSView *)view { return r; }
- (CGRect)convertRectToBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x*k, r.origin.y*k, r.size.width*k, r.size.height*k); }
- (CGSize)convertSizeToBacking:(CGSize)s { double k = fx_screen_scale(); return CGSizeMake(s.width*k, s.height*k); }
- (CGPoint)convertPointToBacking:(CGPoint)p { double k = fx_screen_scale(); return CGPointMake(p.x*k, p.y*k); }
- (CGRect)convertRectFromBacking:(CGRect)r { double k = fx_screen_scale(); return CGRectMake(r.origin.x/k, r.origin.y/k, r.size.width/k, r.size.height/k); }
- (BOOL)isFlipped { return NO; }
- (BOOL)mouseDownCanMoveWindow { return NO; }
- (BOOL)acceptsFirstMouse:(NSEvent *)e { return YES; }
- (BOOL)inLiveResize { return NO; }
@end
// ---- NSWindow ---------------------------------------------------------------------------
static NSMutableArray *fx_windows;
@implementation NSWindow
- (instancetype)initWithContentRect:(CGRect)rect styleMask:(NSUInteger)style backing:(NSUInteger)backing
defer:(BOOL)defer screen:(id)screen {
if ((self = [super init])) {
_styleMask = style;
_alphaValue = 1;
if (!fx_windows) fx_windows = [NSMutableArray array];
[fx_windows addObject:self];
fx_log("[fx] NSWindow requested %.0fx%.0f style=%lu; using fullscreen %.0fx%.0f",
rect.size.width, rect.size.height, (unsigned long)style, fx_screen_points().width, fx_screen_points().height);
self.contentView = [[NSView alloc] initWithFrame:[self frame]];
}
return self;
}
- (instancetype)initWithContentRect:(CGRect)rect styleMask:(NSUInteger)style backing:(NSUInteger)backing defer:(BOOL)defer {
return [self initWithContentRect:rect styleMask:style backing:backing defer:defer screen:nil];
}
- (CGRect)frame { CGSize s = fx_screen_points(); return CGRectMake(0, 0, s.width, s.height); }
- (CGRect)contentRectForFrameRect:(CGRect)r { return r; }
- (CGRect)frameRectForContentRect:(CGRect)r { return r; }
- (CGRect)contentLayoutRect { return [self frame]; }
- (NSScreen *)screen { return NSScreen.mainScreen; }
- (NSInteger)windowNumber { return (NSInteger)[fx_windows indexOfObject:self] + 1; }
- (CGFloat)backingScaleFactor { return fx_screen_scale(); }
- (void)setContentView:(NSView *)view {
NSView *old = _contentView;
if (old) { old.window = nil; fx_main_sync(^{ [old.backing removeFromSuperview]; }); }
_contentView = view;
if (!view) return;
view.window = self;
view.frame = [self frame];
fx_main_sync(^{
UIView *root = fx_root_view();
view.backing.frame = root.bounds;
view.backing.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[root insertSubview:view.backing atIndex:0];
});
if (!_firstResponder) _firstResponder = view;
}
- (BOOL)makeFirstResponder:(NSResponder *)responder { _firstResponder = responder; return YES; }
- (void)makeKeyAndOrderFront:(id)sender {
self.visible = YES;
if (!_keyWindow) {
_keyWindow = YES;
NSNotification *n = [NSNotification notificationWithName:NSWindowDidBecomeKeyNotification object:self];
if ([_delegate respondsToSelector:@selector(windowDidBecomeKey:)]) [_delegate windowDidBecomeKey:n];
[NSNotificationCenter.defaultCenter postNotification:n];
}
}
- (void)makeKeyWindow { [self makeKeyAndOrderFront:nil]; }
- (void)makeMainWindow { }
- (void)orderFront:(id)sender { self.visible = YES; }
- (void)orderFrontRegardless { self.visible = YES; }
- (void)orderOut:(id)sender { self.visible = NO; }
- (void)orderBack:(id)sender { }
- (void)close { self.visible = NO; [fx_windows removeObject:self]; }
- (void)performClose:(id)sender { [self close]; }
- (BOOL)isMiniaturized { return NO; }
- (BOOL)isZoomed { return YES; }
- (BOOL)isOnActiveSpace { return YES; }
- (BOOL)isMainWindow { return _keyWindow; }
- (BOOL)canBecomeKeyWindow { return YES; }
- (BOOL)canBecomeMainWindow { return YES; }
- (BOOL)hasShadow { return NO; }
- (void)setHasShadow:(BOOL)flag { }
- (void)setFrame:(CGRect)frame display:(BOOL)flag { }
- (void)setFrame:(CGRect)frame display:(BOOL)flag animate:(BOOL)animate { }
- (void)setFrameOrigin:(CGPoint)origin { }
- (void)setFrameTopLeftPoint:(CGPoint)point { }
- (void)setContentSize:(CGSize)size { }
- (void)setContentMinSize:(CGSize)size { }
- (void)setContentMaxSize:(CGSize)size { }
- (void)setBackgroundColor:(id)color { }
- (void)setIgnoresMouseEvents:(BOOL)flag { }
- (void)setMouseConfinementRect:(CGRect)rect { }
- (void)setColorSpace:(id)space { }
- (void)registerForDraggedTypes:(NSArray *)types { }
- (void)unregisterDraggedTypes { }
- (void)invalidateCursorRectsForView:(id)view { }
- (void)disableCursorRects { }
- (void)enableCursorRects { }
- (void)toggleFullScreen:(id)sender { }
- (void)miniaturize:(id)sender { }
- (void)deminiaturize:(id)sender { }
- (void)zoom:(id)sender { }
- (void)performZoom:(id)sender { }
- (void)performMiniaturize:(id)sender { }
- (void)center { }
- (void)setShape:(id)shape { }
- (id)standardWindowButton:(NSUInteger)b { return nil; }
- (void)update { }
- (void)display { }
- (void)setPreservesContentDuringLiveResize:(BOOL)flag { }
- (CGRect)convertRectToScreen:(CGRect)r { return r; }
- (CGRect)convertRectFromScreen:(CGRect)r { return r; }
- (CGPoint)convertPointToScreen:(CGPoint)p { return p; }
- (CGPoint)convertPointFromScreen:(CGPoint)p { return p; }
- (CGRect)convertRectToBacking:(CGRect)r { return [self.contentView convertRectToBacking:r]; }
- (CGPoint)mouseLocationOutsideOfEventStream { return fx_mouse_screen; }
- (void)sendEvent:(NSEvent *)e {
NSView *target = self.contentView;
switch (e.type) {
case FXLeftMouseDown: [target mouseDown:e]; break;
case FXLeftMouseUp: [target mouseUp:e]; break;
case FXLeftMouseDragged: [target mouseDragged:e]; break;
case FXRightMouseDown: [target rightMouseDown:e]; break;
case FXRightMouseUp: [target rightMouseUp:e]; break;
case FXRightMouseDragged: [target rightMouseDragged:e]; break;
case FXOtherMouseDown: [target otherMouseDown:e]; break;
case FXOtherMouseUp: [target otherMouseUp:e]; break;
case FXOtherMouseDragged: [target otherMouseDragged:e]; break;
case FXMouseMoved: [target mouseMoved:e]; break;
case FXScrollWheel: [target scrollWheel:e]; break;
case FXKeyDown: [(self.firstResponder ?: target) keyDown:e]; break;
case FXKeyUp: [(self.firstResponder ?: target) keyUp:e]; break;
case FXFlagsChanged: [(self.firstResponder ?: target) flagsChanged:e]; break;
default: break;
}
}
@end
// ---- NSApplication ----------------------------------------------------------------------
static NSMutableArray<NSEvent *> *fx_event_queue;
@interface NSApplication : NSResponder
@property (nonatomic, weak) id delegate;
@property (nonatomic, strong) id mainMenu, windowsMenu, servicesMenu, applicationIconImage;
@property (nonatomic, readonly) NSInteger activationPolicy;
- (BOOL)setActivationPolicy:(NSInteger)policy;
@end
@interface NSMenu : FXShimObject
@end
@implementation NSApplication
+ (instancetype)sharedApplication {
if (!NSApp) {
NSApp = [self new];
fx_log("[fx] +[%s sharedApplication]", class_getName(self));
}
return NSApp;
}
- (instancetype)init {
if ((self = [super init])) {
// A non-nil main menu makes SDL skip NSBundle nib loading and menu construction.
_mainMenu = [NSMenu new];
}
return self;
}
- (BOOL)setActivationPolicy:(NSInteger)policy { _activationPolicy = policy; return YES; }
- (void)finishLaunching {
NSNotification *n = [NSNotification notificationWithName:@"NSApplicationDidFinishLaunchingNotification" object:self];
if ([_delegate respondsToSelector:@selector(applicationDidFinishLaunching:)]) [_delegate applicationDidFinishLaunching:n];
}
- (void)activateIgnoringOtherApps:(BOOL)flag { }
- (BOOL)isActive { return YES; }
- (BOOL)isHidden { return NO; }
- (BOOL)isRunning { return YES; }
- (void)hide:(id)sender { }
- (void)unhide:(id)sender { }
- (void)unhide { }
- (void)unhideAllApplications:(id)sender { }
- (void)hideOtherApplications:(id)sender { }
- (void)setAppleMenu:(id)menu { }
- (void)setPresentationOptions:(NSUInteger)options { }
- (NSUInteger)presentationOptions { return 0; }
- (NSInteger)requestUserAttention:(NSUInteger)type { return 0; }
- (void)cancelUserAttentionRequest:(NSInteger)request { }
- (void)orderFrontStandardAboutPanel:(id)sender { }
- (void)updateWindows { }
- (void)stop:(id)sender { }
- (void)run { fx_log("[fx] -[NSApplication run] called; not supported"); }
- (void)terminate:(id)sender { exit(0); }
- (NSArray *)windows { return [fx_windows copy] ?: @[]; }
- (NSArray *)orderedWindows { return self.windows; }
- (id)keyWindow { for (NSWindow *w in fx_windows) if (w.isKeyWindow) return w; return nil; }
- (id)mainWindow { return self.keyWindow; }
- (id)windowWithWindowNumber:(NSInteger)n { return (n >= 1 && n <= (NSInteger)fx_windows.count) ? fx_windows[n - 1] : nil; }
- (NSEvent *)currentEvent { return nil; }
- (void)postEvent:(NSEvent *)e atStart:(BOOL)atStart {
@synchronized (fx_event_queue) { if (atStart) [fx_event_queue insertObject:e atIndex:0]; else [fx_event_queue addObject:e]; }
}
- (NSEvent *)nextEventMatchingMask:(unsigned long long)mask untilDate:(NSDate *)date inMode:(NSString *)mode dequeue:(BOOL)dequeue {
for (int pass = 0; pass < 2; pass++) {
@synchronized (fx_event_queue) {
if (fx_event_queue.count) {
NSEvent *e = fx_event_queue[0];
if (dequeue) [fx_event_queue removeObjectAtIndex:0];
return e;
}
}
if (pass == 0) {
// Let UIKit deliver touches and run main-queue work, as SDL's UIKit backend does.
NSTimeInterval wait = date ? MAX(0, MIN([date timeIntervalSinceNow], 0.05)) : 0;
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource) { }
if (wait > 0 && !fx_event_queue.count) CFRunLoopRunInMode(kCFRunLoopDefaultMode, wait, true);
}
}
return nil;
}
- (void)discardEventsMatchingMask:(unsigned long long)mask beforeEvent:(NSEvent *)e { }
- (void)sendEvent:(NSEvent *)e { [e.window sendEvent:e]; }
- (NSInteger)runModalForWindow:(id)window { return 0; }
- (void)stopModalWithCode:(NSInteger)code { }
- (void)registerUserDefaults { }
@end
// Queue a mouse event from UIKit (main thread).
void fx_post_mouse(NSUInteger type, CGPoint uikit_point, NSInteger button, NSInteger clicks) {
CGSize s = fx_screen_points();
CGFloat k = fx_game_per_uikit();
CGPoint p = CGPointMake(uikit_point.x * k, s.height - uikit_point.y * k);
CGFloat dx = p.x - fx_mouse_screen.x, dy = -(p.y - fx_mouse_screen.y);
fx_mouse_screen = p;
if (type == FXLeftMouseDown || type == FXRightMouseDown || type == FXOtherMouseDown) fx_mouse_buttons |= (1u << button);
if (type == FXLeftMouseUp || type == FXRightMouseUp || type == FXOtherMouseUp) fx_mouse_buttons &= ~(1u << button);
NSEvent *e = [NSEvent new];
e.type = type;
e.locationInWindow = p;
e.window = fx_windows.firstObject;
e.buttonNumber = button;
e.clickCount = clicks;
e.modifierFlags = fx_current_modifier_flags();
e.deltaX = dx;
e.deltaY = dy;
e.timestamp = CACurrentMediaTime();
[NSApp postEvent:e atStart:NO];
}
void fx_post_key_event(NSUInteger type, unsigned short keyCode, NSString *chars, NSUInteger flags) {
NSEvent *e = [NSEvent new];
e.type = type;
e.keyCode = keyCode;
e.characters = chars ?: @"";
e.modifierFlags = flags;
e.locationInWindow = fx_mouse_screen;
e.window = fx_windows.firstObject;
e.timestamp = CACurrentMediaTime();
[NSApp postEvent:e atStart:NO];
}
void fx_post_scroll(CGPoint uikit_point, CGFloat dx, CGFloat dy) {
NSEvent *e = [NSEvent new];
CGSize s = fx_screen_points();
e.type = FXScrollWheel;
CGFloat k = fx_game_per_uikit();
e.locationInWindow = CGPointMake(uikit_point.x * k, s.height - uikit_point.y * k);
e.window = fx_windows.firstObject;
e.deltaX = e.scrollingDeltaX = dx;
e.deltaY = e.scrollingDeltaY = dy;
e.timestamp = CACurrentMediaTime();
[NSApp postEvent:e atStart:NO];
}
__attribute__((constructor)) static void fx_appkit_init(void) { fx_event_queue = [NSMutableArray array]; }
// ---- Inert classes ----------------------------------------------------------------------
@implementation NSMenu
- (instancetype)initWithTitle:(NSString *)title { return [super init]; }
- (id)addItemWithTitle:(NSString *)t action:(SEL)a keyEquivalent:(NSString *)k { return [NSClassFromString(@"NSMenuItem") new]; }
- (void)addItem:(id)item { }
- (id)itemWithTitle:(NSString *)t { return nil; }
+ (void)setMenuBarVisible:(BOOL)visible { }
+ (BOOL)menuBarVisible { return NO; }
@end
@interface NSMenuItem : FXShimObject
@end
@implementation NSMenuItem
+ (instancetype)separatorItem { return [self new]; }
- (instancetype)initWithTitle:(NSString *)t action:(SEL)a keyEquivalent:(NSString *)k { return [super init]; }
@end
@interface NSCursor : FXShimObject
@end
@implementation NSCursor
#define FX_CURSOR(name) + (instancetype)name { static NSCursor *c; static dispatch_once_t o; dispatch_once(&o, ^{ c = [NSCursor new]; }); return c; }
FX_CURSOR(arrowCursor) FX_CURSOR(IBeamCursor) FX_CURSOR(crosshairCursor) FX_CURSOR(closedHandCursor)
FX_CURSOR(openHandCursor) FX_CURSOR(pointingHandCursor) FX_CURSOR(resizeLeftRightCursor)
FX_CURSOR(resizeUpDownCursor) FX_CURSOR(operationNotAllowedCursor) FX_CURSOR(currentCursor)
FX_CURSOR(invisibleCursor) FX_CURSOR(currentSystemCursor)
+ (void)hide { }
+ (void)unhide { }
+ (void)setHiddenUntilMouseMoves:(BOOL)flag { }
- (instancetype)initWithImage:(id)image hotSpot:(CGPoint)point { return [super init]; }
- (void)set { }
- (void)push { }
- (void)pop { }
@end
@interface NSImage : FXShimObject
@end
@implementation NSImage
- (instancetype)initWithSize:(CGSize)size { return [super init]; }
- (instancetype)initWithData:(NSData *)data { return [super init]; }
- (instancetype)initWithContentsOfFile:(NSString *)path { return [super init]; }
- (void)addRepresentation:(id)rep { }
- (void)lockFocus { }
- (void)unlockFocus { }
- (void)drawInRect:(CGRect)r fromRect:(CGRect)f operation:(NSUInteger)op fraction:(CGFloat)fr { }
@end
@interface NSColorSpace : FXShimObject
+ (instancetype)sRGBColorSpace;
@end
@interface NSBitmapImageRep : FXShimObject
@end
// Decode statistics, logged every 500 images: how many took the direct-copy path, wall time.
#include <stdatomic.h>
static _Atomic long fx_decode_count, fx_decode_direct, fx_decode_pixels, fx_decode_micros;
static void fx_log_format_once(size_t bpc, size_t bpp, uint32_t alpha, uint32_t order, int model) {
static NSMutableSet *seen;
NSString *key = [NSString stringWithFormat:@"bpc %zu bpp %zu alpha %u order %#x model %d", bpc, bpp, alpha, order, model];
@synchronized (NSBitmapImageRep.class) {
if (!seen) seen = [NSMutableSet set];
if ([seen containsObject:key]) return;
[seen addObject:key];
}
fx_log("[fx] ImageIO layout needing conversion: %s", key.UTF8String);
}
static void fx_decode_stats(long pixels, double seconds) {
static double start;
long n = atomic_fetch_add(&fx_decode_count, 1) + 1;
atomic_fetch_add(&fx_decode_pixels, pixels);
atomic_fetch_add(&fx_decode_micros, (long)(seconds * 1e6));
if (n == 1) start = CACurrentMediaTime();
if (n % 500 == 0)
fx_log("[fx] decoded %ld images (%ld wuffs/direct), %.0f Mpx, %.1fs wall, %.1fs summed decode time", n,
(long)fx_decode_direct, fx_decode_pixels / 1e6, CACurrentMediaTime() - start, fx_decode_micros / 1e6);
}
@implementation NSBitmapImageRep {
unsigned char *_data;
NSInteger _w, _h, _rowBytes, _bpp, _spp;
}
- (instancetype)initWithBitmapDataPlanes:(unsigned char **)planes pixelsWide:(NSInteger)w pixelsHigh:(NSInteger)h
bitsPerSample:(NSInteger)bps samplesPerPixel:(NSInteger)spp hasAlpha:(BOOL)alpha
isPlanar:(BOOL)planar colorSpaceName:(NSString *)cs bytesPerRow:(NSInteger)rowBytes
bitsPerPixel:(NSInteger)bpp {
if ((self = [super init])) {
_w = w; _h = h; _spp = spp; _bpp = bpp ? bpp : bps * spp;
_rowBytes = rowBytes ? rowBytes : (w * _bpp + 7) / 8;
_data = calloc((size_t)(_rowBytes * h), 1);
}
return self;
}
// Decodes like AppKit does for PNG: tightly packed, non-premultiplied, native-endian samples,
// keeping the source channel count (gray, gray+alpha, RGB, RGBA) and 8 or 16 bits per sample.
// Factorio's preload_image() copies and premultiplies this buffer itself.
- (instancetype)initWithData:(NSData *)data {
if (!(self = [super init])) return nil;
double fx_t0 = CACurrentMediaTime();
uint8_t *png; int pw, ph, pc;
if (fx_png_decode(data.bytes, data.length, &png, &pw, &ph, &pc)) {
_data = png; _w = pw; _h = ph; _spp = pc; _bpp = 8 * pc; _rowBytes = (NSInteger)pw * pc;
fx_decode_direct++;
fx_decode_stats(_w * _h, CACurrentMediaTime() - fx_t0);
return self;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data,
(__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCache: @NO});
CGImageRef image = source ? CGImageSourceCreateImageAtIndex(source, 0, NULL) : NULL;
if (source) CFRelease(source);
if (!image) return nil;
BOOL ok = [self fx_unpack:image] || [self fx_render:image];
CGImageRelease(image);
fx_decode_stats(_w * _h, CACurrentMediaTime() - fx_t0);
return ok ? self : nil;
}
+ (instancetype)imageRepWithData:(NSData *)data { return [[self alloc] initWithData:data]; }
- (BOOL)fx_unpack:(CGImageRef)image {
size_t bpc = CGImageGetBitsPerComponent(image), bpp = CGImageGetBitsPerPixel(image);
CGColorSpaceModel model = CGColorSpaceGetModel(CGImageGetColorSpace(image));
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(image);
CGBitmapInfo info = CGImageGetBitmapInfo(image);
if ((bpc != 8 && bpc != 16) || (info & kCGBitmapFloatComponents)) return NO;
size_t color = model == kCGColorSpaceModelMonochrome ? 1 : model == kCGColorSpaceModelRGB ? 3 : 0;
if (!color) return NO;
BOOL hasAlpha = alpha == kCGImageAlphaLast || alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaPremultipliedLast || alpha == kCGImageAlphaPremultipliedFirst;
BOOL premultiplied = alpha == kCGImageAlphaPremultipliedLast || alpha == kCGImageAlphaPremultipliedFirst;
BOOL alphaFirst = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaNoneSkipFirst;
BOOL skip = alpha == kCGImageAlphaNoneSkipLast || alpha == kCGImageAlphaNoneSkipFirst;
size_t srcComponents = bpp / bpc, outComponents = color + (hasAlpha ? 1 : 0);
if (srcComponents != color + ((hasAlpha || skip) ? 1 : 0)) return NO;
CGBitmapInfo order = info & kCGBitmapByteOrderMask;
BOOL reversed = (bpc == 8 && (order == kCGBitmapByteOrder32Little || order == kCGBitmapByteOrder16Little));
BOOL littleSamples = bpc == 16 && order == kCGBitmapByteOrder16Little;
size_t w = CGImageGetWidth(image), h = CGImageGetHeight(image);
size_t sampleBytes = bpc / 8, srcPixel = bpp / 8;
_w = w; _h = h; _spp = outComponents; _bpp = outComponents * bpc; _rowBytes = w * outComponents * sampleBytes;
_data = malloc(_rowBytes * h);
// Decide from metadata before touching pixels: each pixel access below decodes the PNG.
// Direct: decoded layout already matches (non-premultiplied R G B [A], 8-bit or 16-bit LE).
BOOL direct = !premultiplied && !alphaFirst && !skip && !reversed && (bpc == 8 || littleSamples);
if (!direct) {
fx_log_format_once(bpc, bpp, alpha, order, model);
// One vectorized vImage decode+conversion into the target layout, keeping the image's
// colour space (no colour management changes).
vImage_CGImageFormat target = {
(uint32_t)bpc, (uint32_t)(outComponents * bpc), CGImageGetColorSpace(image),
(hasAlpha ? kCGImageAlphaLast : kCGImageAlphaNone) | (bpc == 16 ? kCGBitmapByteOrder16Little : kCGBitmapByteOrderDefault),
0, NULL, kCGRenderingIntentDefault};
vImage_Buffer buffer = {_data, h, w, (size_t)_rowBytes};
if (vImageBuffer_InitWithCGImage(&buffer, &target, NULL, image, kvImageNoAllocate) == kvImageNoError) return YES;
fx_log("[fx] vImage conversion failed; using scalar path");
}
CFDataRef pixels = CGDataProviderCopyData(CGImageGetDataProvider(image));
if (!pixels) return NO;
const uint8_t *src = CFDataGetBytePtr(pixels);
size_t srcRow = CGImageGetBytesPerRow(image);
if (direct) {
if (srcRow == (size_t)_rowBytes) memcpy(_data, src, _rowBytes * h);
else for (size_t y = 0; y < h; y++) memcpy(_data + y * _rowBytes, src + y * srcRow, _rowBytes);
fx_decode_direct++;
CFRelease(pixels);
return YES;
}
for (size_t y = 0; y < h; y++) {
for (size_t x = 0; x < w; x++) {
const uint8_t *p = src + y * srcRow + x * srcPixel;
uint32_t s[4] = {0};
for (size_t c = 0; c < srcComponents; c++) {
size_t i = reversed ? srcComponents - 1 - c : c;
s[c] = bpc == 8 ? p[i] : littleSamples ? (uint32_t)(p[2*i] | p[2*i+1] << 8) : (uint32_t)(p[2*i] << 8 | p[2*i+1]);
}
uint32_t out[4], a = 0;
size_t first = alphaFirst ? 1 : 0;
for (size_t c = 0; c < color; c++) out[c] = s[first + c];
if (hasAlpha) {
a = alphaFirst ? s[0] : s[color];
uint32_t max = bpc == 8 ? 255 : 65535;
if (premultiplied && a && a < max)
for (size_t c = 0; c < color; c++) out[c] = MIN(max, (out[c] * max + a / 2) / a);
out[color] = a;
}
uint8_t *d = _data + y * _rowBytes + x * outComponents * sampleBytes;
for (size_t c = 0; c < outComponents; c++) {
if (bpc == 8) d[c] = (uint8_t)out[c];
else ((uint16_t *)d)[c] = (uint16_t)out[c];
}
}
}
CFRelease(pixels);
return YES;
}
// Fallback for indexed, CMYK and other layouts: draw into RGBA8 and un-premultiply.
- (BOOL)fx_render:(CGImageRef)image {
size_t w = CGImageGetWidth(image), h = CGImageGetHeight(image);
free(_data);
_w = w; _h = h; _spp = 4; _bpp = 32; _rowBytes = w * 4;
_data = calloc(_rowBytes * h, 1);
CGColorSpaceRef rgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
CGContextRef ctx = CGBitmapContextCreate(_data, w, h, 8, _rowBytes, rgb, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGColorSpaceRelease(rgb);
if (!ctx) return NO;
CGContextSetBlendMode(ctx, kCGBlendModeCopy);
CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), image);
CGContextRelease(ctx);
for (size_t i = 0; i < w * h; i++) {
uint8_t *p = _data + i * 4, a = p[3];
if (a && a < 255) for (int c = 0; c < 3; c++) p[c] = (uint8_t)MIN(255, (p[c] * 255 + a / 2) / a);
}
return YES;
}
- (void)dealloc { free(_data); }
- (unsigned char *)bitmapData { return _data; }
- (NSInteger)pixelsWide { return _w; }
- (NSInteger)pixelsHigh { return _h; }
- (NSInteger)bitsPerPixel { return _bpp; }
- (NSInteger)samplesPerPixel { return _spp; }
- (NSInteger)bytesPerRow { return _rowBytes; }
- (CGSize)size { return CGSizeMake(_w, _h); }
- (id)colorSpace { return [NSColorSpace sRGBColorSpace]; }
- (id)bitmapImageRepByRetaggingWithColorSpace:(id)cs { return self; }
- (id)bitmapImageRepByConvertingToColorSpace:(id)cs renderingIntent:(NSInteger)intent { return self; }
@end
@interface FXNSColor : FXShimObject
@end
@implementation FXNSColor
+ (instancetype)blackColor { return [self new]; }
+ (instancetype)clearColor { return [self new]; }
+ (instancetype)whiteColor { return [self new]; }
+ (instancetype)colorWithRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a { return [self new]; }
+ (instancetype)colorWithSRGBRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a { return [self new]; }
- (void)set { }
- (void)setFill { }
- (void)setStroke { }
@end
@implementation NSColorSpace
#define FX_SPACE(name) + (instancetype)name { static id s; static dispatch_once_t o; dispatch_once(&o, ^{ s = [self new]; }); return s; }
FX_SPACE(sRGBColorSpace) FX_SPACE(genericRGBColorSpace) FX_SPACE(deviceRGBColorSpace)
- (NSData *)ICCProfileData { return nil; }
- (CGColorSpaceRef)CGColorSpace { return NULL; }
@end
@interface NSGraphicsContext : FXShimObject
@end
@implementation NSGraphicsContext
+ (id)currentContext { return nil; }
+ (void)setCurrentContext:(id)ctx { }
+ (void)saveGraphicsState { }
+ (void)restoreGraphicsState { }
+ (id)graphicsContextWithBitmapImageRep:(id)rep { return nil; }
@end
@interface NSBezierPath : FXShimObject
@end
@implementation NSBezierPath
+ (instancetype)bezierPath { return [self new]; }
+ (instancetype)bezierPathWithRect:(CGRect)r { return [self new]; }
- (void)appendBezierPathWithRect:(CGRect)r { }
- (void)addClip { }
- (void)fill { }
@end
@interface NSAlert : FXShimObject
@property (copy) NSString *messageText, *informativeText;
@property NSUInteger alertStyle;
@end
@implementation NSAlert
- (id)addButtonWithTitle:(NSString *)title { fx_log("[fx] NSAlert button: %s", title.UTF8String); return nil; }
- (NSInteger)runModal {
fx_log("[fx] NSAlert: %s | %s", self.messageText.UTF8String ?: "", self.informativeText.UTF8String ?: "");
return 1000; // NSAlertFirstButtonReturn
}
- (void)beginSheetModalForWindow:(id)w completionHandler:(void (^)(NSInteger))handler { NSInteger r = [self runModal]; if (handler) handler(r); }
- (id)window { return nil; }
@end
@interface NSPasteboard : FXShimObject
@end
@implementation NSPasteboard
+ (instancetype)generalPasteboard { static NSPasteboard *p; static dispatch_once_t o; dispatch_once(&o, ^{ p = [NSPasteboard new]; }); return p; }
- (NSInteger)changeCount { __block NSInteger c; fx_main_sync(^{ c = UIPasteboard.generalPasteboard.changeCount; }); return c; }
- (NSString *)stringForType:(NSString *)type { __block NSString *s; fx_main_sync(^{ s = UIPasteboard.generalPasteboard.string; }); return s; }
- (BOOL)setString:(NSString *)s forType:(NSString *)type { fx_main_sync(^{ UIPasteboard.generalPasteboard.string = s; }); return YES; }
- (NSInteger)declareTypes:(NSArray *)types owner:(id)owner { return [self changeCount]; }
- (NSInteger)clearContents { return [self changeCount]; }
- (NSString *)availableTypeFromArray:(NSArray *)types { return types.firstObject; }
- (id)propertyListForType:(NSString *)type { return nil; }
- (NSData *)dataForType:(NSString *)type { return nil; }
@end
@interface NSWorkspace : FXShimObject
@end
@implementation NSWorkspace
+ (instancetype)sharedWorkspace { static NSWorkspace *w; static dispatch_once_t o; dispatch_once(&o, ^{ w = [NSWorkspace new]; }); return w; }
- (BOOL)openURL:(NSURL *)url { fx_log("[fx] openURL %s", url.absoluteString.UTF8String); return NO; }
- (void)activateFileViewerSelectingURLs:(NSArray *)urls { }
- (id)notificationCenter { return NSNotificationCenter.defaultCenter; }
@end
@interface NSRunningApplication : FXShimObject
@end
@implementation NSRunningApplication
+ (instancetype)currentApplication { return [self new]; }
+ (NSArray *)runningApplicationsWithBundleIdentifier:(NSString *)bundle { return @[]; }
- (BOOL)activateWithOptions:(NSUInteger)options { return YES; }
- (BOOL)isActive { return YES; }
- (BOOL)isFinishedLaunching { return YES; }
@end
@interface NSOpenGLPixelFormat : FXShimObject
@end
@implementation NSOpenGLPixelFormat
- (instancetype)initWithAttributes:(const uint32_t *)attributes { fx_log("[fx] OpenGL pixel format requested; unsupported"); return nil; }
- (void *)CGLPixelFormatObj { return NULL; }
@end
@interface NSOpenGLContext : FXShimObject
@end
@implementation NSOpenGLContext
- (instancetype)initWithFormat:(id)format shareContext:(id)share { fx_log("[fx] OpenGL context requested; unsupported"); return nil; }
+ (void)clearCurrentContext { }
+ (id)currentContext { return nil; }
- (void)makeCurrentContext { }
- (void)flushBuffer { }
- (void)update { }
- (void)setView:(id)view { }
- (id)view { return nil; }
- (void *)CGLContextObj { return NULL; }
@end