// 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 #import #import #import #import #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 *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 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