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.
922 lines
43 KiB
Objective-C
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
|