// Keyboard emulation: everything becomes AppKit key events with macOS virtual key codes, // which Factorio's SDL Cocoa backend translates exactly as on a Mac. // * hardware keyboards (UIPress HID usage -> mac key code) // * the iOS soft keyboard for text (insertText -> key down/up with characters) // * an on-screen overlay: WASD stick and common Factorio keys, sticky Shift/Ctrl #import #import "fx.h" void fx_post_key_event(NSUInteger type, unsigned short keyCode, NSString *chars, NSUInteger flags); // macOS virtual key codes. enum { MK_A = 0, MK_S = 1, MK_D = 2, MK_F = 3, MK_W = 13, MK_E = 14, MK_R = 15, MK_T = 17, MK_Q = 12, MK_M = 46, MK_RETURN = 36, MK_TAB = 48, MK_SPACE = 49, MK_DELETE = 51, MK_ESCAPE = 53, MK_RCMD = 54, MK_LCMD = 55, MK_LSHIFT = 56, MK_CAPS = 57, MK_LOPT = 58, MK_LCTRL = 59, MK_RSHIFT = 60, MK_ROPT = 61, MK_RCTRL = 62, MK_NONE = 0xFFFF, }; // AppKit modifier flags plus the device-dependent side bits SDL uses to tell left from right. #define F_CAPS (1u << 16) #define F_SHIFT (1u << 17) #define F_CTRL (1u << 18) #define F_OPT (1u << 19) #define F_CMD (1u << 20) static const struct { unsigned short key; NSUInteger flag, side; } fx_modifiers[] = { {MK_LCTRL, F_CTRL, 0x0001}, {MK_LSHIFT, F_SHIFT, 0x0002}, {MK_RSHIFT, F_SHIFT, 0x0004}, {MK_LCMD, F_CMD, 0x0008}, {MK_RCMD, F_CMD, 0x0010}, {MK_LOPT, F_OPT, 0x0020}, {MK_ROPT, F_OPT, 0x0040}, {MK_RCTRL, F_CTRL, 0x2000}, }; static NSUInteger fx_flags; // current modifier state as AppKit reports it FX_EXPORT NSUInteger fx_current_modifier_flags(void) { return fx_flags; } static void fx_recompute_flags(NSUInteger sides) { NSUInteger flags = (fx_flags & F_CAPS) | sides; for (size_t i = 0; i < sizeof fx_modifiers / sizeof *fx_modifiers; i++) if (sides & fx_modifiers[i].side) flags |= fx_modifiers[i].flag; fx_flags = flags; } // Press or release one mac key. Modifier keys become flagsChanged events. FX_EXPORT void fx_key(unsigned short key, BOOL down, NSString *chars) { if (key == MK_NONE) return; for (size_t i = 0; i < sizeof fx_modifiers / sizeof *fx_modifiers; i++) { if (fx_modifiers[i].key != key) continue; NSUInteger sides = fx_flags & 0xFFFF; sides = down ? (sides | fx_modifiers[i].side) : (sides & ~fx_modifiers[i].side); fx_recompute_flags(sides); fx_post_key_event(12, key, nil, fx_flags); return; } if (key == MK_CAPS) { if (down) fx_flags ^= F_CAPS; fx_post_key_event(12, key, nil, fx_flags); return; } fx_post_key_event(down ? 10 : 11, key, chars, fx_flags); } FX_EXPORT void fx_tap_key(unsigned short key, NSString *chars) { fx_key(key, YES, chars); fx_key(key, NO, chars); } // ---- HID usage (UIKeyboardHIDUsage) -> mac virtual key code -------------------------------- static unsigned short fx_mac_key_for_hid(long usage) { static const unsigned short letters[26] = {0, 11, 8, 2, 14, 3, 5, 4, 34, 38, 40, 37, 46, 45, 31, 35, 12, 15, 1, 17, 32, 9, 13, 7, 16, 6}; static const unsigned short digits[10] = {18, 19, 20, 21, 23, 22, 26, 28, 25, 29}; // 1..9, 0 static const unsigned short fkeys[12] = {122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111}; static const unsigned short keypad[10] = {83, 84, 85, 86, 87, 88, 89, 91, 92, 82}; // KP1..KP9, KP0 if (usage >= 4 && usage <= 29) return letters[usage - 4]; if (usage >= 30 && usage <= 39) return digits[usage - 30]; if (usage >= 58 && usage <= 69) return fkeys[usage - 58]; if (usage >= 89 && usage <= 98) return keypad[usage - 89]; switch (usage) { case 40: return MK_RETURN; case 41: return MK_ESCAPE; case 42: return MK_DELETE; case 43: return MK_TAB; case 44: return MK_SPACE; case 45: return 27; case 46: return 24; case 47: return 33; case 48: return 30; case 49: return 42; case 50: return 42; case 51: return 41; case 52: return 39; case 53: return 50; case 54: return 43; case 55: return 47; case 56: return 44; case 57: return MK_CAPS; case 73: return 114; case 74: return 115; case 75: return 116; case 76: return 117; case 77: return 119; case 78: return 121; case 79: return 124; case 80: return 123; case 81: return 125; case 82: return 126; case 83: return 71; case 84: return 75; case 85: return 67; case 86: return 78; case 87: return 69; case 88: return 76; case 99: return 65; case 100: return 10; case 103: return 81; case 104: return 105; case 105: return 107; case 106: return 113; case 107: return 106; // F13..F16 case 224: return MK_LCTRL; case 225: return MK_LSHIFT; case 226: return MK_LOPT; case 227: return MK_LCMD; case 228: return MK_RCTRL; case 229: return MK_RSHIFT; case 230: return MK_ROPT; case 231: return MK_RCMD; default: return MK_NONE; } } static NSString *fx_text_chars(NSString *chars) { if (chars.length == 0) return nil; unichar c = [chars characterAtIndex:0]; return (c < 0x20 || c == 0x7f || (c >= 0xF700 && c <= 0xF8FF)) ? nil : chars; } FX_EXPORT BOOL fx_handle_presses(NSSet *presses, BOOL down) { BOOL handled = NO; for (UIPress *press in presses) { UIKey *key = press.key; if (!key) continue; unsigned short mac = fx_mac_key_for_hid(key.keyCode); if (mac == MK_NONE) { fx_log("[fx] unmapped HID key usage %ld", (long)key.keyCode); continue; } fx_key(mac, down, down ? fx_text_chars(key.characters) : nil); handled = YES; } return handled; } // Mac key code for a typed character, so soft-keyboard letters also work as hotkeys. static unsigned short fx_mac_key_for_char(unichar c, BOOL *shift) { // Index = mac key code 0..47 (US layout); '\1' marks codes that are not characters. static const char plain[] = "asdfhgzxcv\1bqweryt123465=97-80]ou[ip\1lj'k;\\,/nm."; static const char shifted[] = "ASDFHGZXCV\1BQWERYT!@#$^%+(&_*)}OU{IP\1LJ\"K:|"; *shift = NO; if (c == ' ') return MK_SPACE; if (c == '`') return 50; if (c == '~') { *shift = YES; return 50; } if (c >= 0x80) return MK_NONE; for (unsigned short i = 0; i < sizeof plain - 1; i++) { if (plain[i] == c) return i; if (shifted[i] == c) { *shift = YES; return i; } } return MK_NONE; } static UIView *fx_make_accessory_bar(void); static void fx_release_sticky(void); // ---- Soft keyboard ------------------------------------------------------------------------- @interface FXTextInputView : UIView @end @implementation FXTextInputView - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)hasText { return YES; } - (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; } - (UITextAutocapitalizationType)autocapitalizationType { return UITextAutocapitalizationTypeNone; } - (UITextSpellCheckingType)spellCheckingType { return UITextSpellCheckingTypeNo; } - (UITextSmartQuotesType)smartQuotesType { return UITextSmartQuotesTypeNo; } - (UITextSmartDashesType)smartDashesType { return UITextSmartDashesTypeNo; } - (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; } - (UIView *)inputAccessoryView { static UIView *bar; if (!bar) bar = fx_make_accessory_bar(); return bar; } - (void)insertText:(NSString *)text { [text enumerateSubstringsInRange:NSMakeRange(0, text.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *s, NSRange r, NSRange e, BOOL *stop) { unichar c = [s characterAtIndex:0]; if (c == '\n') { fx_tap_key(MK_RETURN, nil); return; } BOOL shift = NO; unsigned short key = fx_mac_key_for_char(c, &shift); if (shift) fx_key(MK_LSHIFT, YES, nil); // Characters without a mac key (e.g. accented letters) still arrive as text via key 0xFFFE. fx_key(key == MK_NONE ? 0x7F : key, YES, s); fx_key(key == MK_NONE ? 0x7F : key, NO, nil); if (shift) fx_key(MK_LSHIFT, NO, nil); fx_release_sticky(); }]; } - (void)deleteBackward { fx_tap_key(MK_DELETE, nil); fx_release_sticky(); } // Hardware keys are handled as raw presses so they are not also delivered as text. - (void)pressesBegan:(NSSet *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, YES)) [super pressesBegan:p withEvent:e]; } - (void)pressesEnded:(NSSet *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesEnded:p withEvent:e]; } - (void)pressesCancelled:(NSSet *)p withEvent:(UIPressesEvent *)e { if (!fx_handle_presses(p, NO)) [super pressesCancelled:p withEvent:e]; } @end // ---- Key buttons ------------------------------------------------------------------------- // A button that presses a mac key while held; sticky buttons latch until the next click or key. @interface FXKeyButton : UIButton @property unsigned short key; @property (copy) NSString *chars; @property (nonatomic) BOOL sticky, latched; @end static NSMutableArray *fx_sticky_buttons; @implementation FXKeyButton + (instancetype)keyButton:(NSString *)title key:(unsigned short)key chars:(NSString *)chars frame:(CGRect)frame { FXKeyButton *b = [self buttonWithType:UIButtonTypeCustom]; b.frame = frame; b.key = key; b.chars = chars; [b setTitle:title forState:UIControlStateNormal]; b.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold]; b.titleLabel.adjustsFontSizeToFitWidth = YES; [b setTitleColor:[UIColor colorWithWhite:1 alpha:0.9] forState:UIControlStateNormal]; b.backgroundColor = [UIColor colorWithWhite:0 alpha:0.35]; b.layer.cornerRadius = 8; b.layer.borderWidth = 1; b.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.3].CGColor; [b addTarget:b action:@selector(fx_down) forControlEvents:UIControlEventTouchDown]; [b addTarget:b action:@selector(fx_up) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchCancel]; return b; } - (void)setSticky:(BOOL)sticky { _sticky = sticky; if (!fx_sticky_buttons) fx_sticky_buttons = [NSMutableArray array]; if (sticky) [fx_sticky_buttons addObject:self]; } - (void)fx_set_latched:(BOOL)latched { if (latched == _latched) return; _latched = latched; self.backgroundColor = latched ? [UIColor colorWithRed:0.9 green:0.55 blue:0.1 alpha:0.75] : [UIColor colorWithWhite:0 alpha:0.35]; fx_key(self.key, latched, nil); } - (void)fx_down { if (self.key == MK_NONE) return; if (self.sticky) { // Latching one copy of a modifier (overlay or keyboard bar) mirrors to the others. BOOL latch = !self.latched; for (FXKeyButton *b in fx_sticky_buttons) if (b.key == self.key && b != self) { b->_latched = latch; b.backgroundColor = self.backgroundColor; } [self fx_set_latched:latch]; for (FXKeyButton *b in fx_sticky_buttons) if (b.key == self.key) b.backgroundColor = self.backgroundColor; return; } fx_key(self.key, YES, self.chars); } - (void)fx_up { if (self.key == MK_NONE || self.sticky) return; fx_key(self.key, NO, nil); fx_release_sticky(); } @end static void fx_release_sticky(void) { for (FXKeyButton *b in fx_sticky_buttons) if (b.latched) [b fx_down]; } // Extra keys shown above the iOS keyboard (which has no Tab, Esc, Ctrl, Alt, arrows or F-keys). static UIView *fx_make_accessory_bar(void) { CGFloat h = 46, s = 8; UIView *bar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, h)]; bar.backgroundColor = [UIColor colorWithWhite:0.12 alpha:0.96]; UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:bar.bounds]; scroll.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; scroll.showsHorizontalScrollIndicator = NO; scroll.delaysContentTouches = NO; [bar addSubview:scroll]; NSArray *keys = @[ @[@"Esc", @(MK_ESCAPE), @""], @[@"Tab", @(MK_TAB), @""], @[@"Ctrl", @(MK_LCTRL), @"sticky"], @[@"Alt", @(MK_LOPT), @"sticky"], @[@"⇧", @(MK_LSHIFT), @"sticky"], @[@"←", @123, @""], @[@"↑", @126, @""], @[@"↓", @125, @""], @[@"→", @124, @""], @[@"Enter", @(MK_RETURN), @""], @[@"Del", @117, @""], @[@"`", @50, @"`"], @[@"Home", @115, @""], @[@"End", @119, @""], @[@"PgUp", @116, @""], @[@"PgDn", @121, @""], @[@"F1", @122, @""], @[@"F2", @120, @""], @[@"F3", @99, @""], @[@"F4", @118, @""], @[@"F5", @96, @""], @[@"F6", @97, @""], @[@"F7", @98, @""], @[@"F8", @100, @""], @[@"F9", @101, @""], @[@"F10", @109, @""], @[@"F11", @103, @""], @[@"F12", @111, @""], ]; CGFloat x = 56; // clear of the Dynamic Island in landscape for (NSArray *k in keys) { NSString *title = k[0], *extra = k[2]; CGFloat w = MAX(44, title.length * 11 + 22); BOOL sticky = [extra isEqualToString:@"sticky"]; FXKeyButton *b = [FXKeyButton keyButton:title key:[k[1] unsignedShortValue] chars:(!sticky && extra.length) ? extra : nil frame:CGRectMake(x, 5, w, h - 10)]; b.sticky = sticky; [scroll addSubview:b]; x += w + s; } scroll.contentSize = CGSizeMake(x + 48, h); return bar; } // ---- Overlay --------------------------------------------------------------------------------- @interface FXStick : UIView @end @implementation FXStick { UIView *_knob; BOOL _held[4]; // W A S D } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { self.layer.cornerRadius = frame.size.width / 2; self.backgroundColor = [UIColor colorWithWhite:1 alpha:0.12]; self.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.35].CGColor; self.layer.borderWidth = 1; _knob = [[UIView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width * 0.42, frame.size.width * 0.42)]; _knob.layer.cornerRadius = _knob.bounds.size.width / 2; _knob.backgroundColor = [UIColor colorWithWhite:1 alpha:0.4]; _knob.userInteractionEnabled = NO; _knob.center = CGPointMake(frame.size.width / 2, frame.size.height / 2); [self addSubview:_knob]; } return self; } - (void)fx_update:(CGPoint)p { CGFloat r = self.bounds.size.width / 2; CGPoint c = CGPointMake(r, r); CGFloat dx = p.x - c.x, dy = p.y - c.y, len = hypot(dx, dy); if (len > r) { dx = dx / len * r; dy = dy / len * r; } _knob.center = CGPointMake(c.x + dx, c.y + dy); CGFloat dead = r * 0.3; BOOL want[4] = {dy < -dead, dx < -dead, dy > dead, dx > dead}; static const unsigned short keys[4] = {MK_W, MK_A, MK_S, MK_D}; static NSString *const chars[4] = {@"w", @"a", @"s", @"d"}; for (int i = 0; i < 4; i++) if (want[i] != _held[i]) { _held[i] = want[i]; fx_key(keys[i], want[i], want[i] ? chars[i] : nil); } } - (void)touchesBegan:(NSSet *)t withEvent:(UIEvent *)e { [self fx_update:[t.anyObject locationInView:self]]; } - (void)touchesMoved:(NSSet *)t withEvent:(UIEvent *)e { [self fx_update:[t.anyObject locationInView:self]]; } - (void)touchesEnded:(NSSet *)t withEvent:(UIEvent *)e { [self fx_update:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2)]; } - (void)touchesCancelled:(NSSet *)t withEvent:(UIEvent *)e { [self touchesEnded:t withEvent:e]; } @end @interface FXControls : UIView @end @implementation FXControls { FXTextInputView *_text; NSMutableArray *_gameControls; } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hit = [super hitTest:point withEvent:event]; return hit == self ? nil : hit; // empty overlay space belongs to the game } - (FXKeyButton *)fx_button:(NSString *)title key:(unsigned short)key chars:(NSString *)chars frame:(CGRect)frame { FXKeyButton *b = [FXKeyButton keyButton:title key:key chars:chars frame:frame]; [self addSubview:b]; return b; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _gameControls = [NSMutableArray array]; _text = [[FXTextInputView alloc] initWithFrame:CGRectZero]; [self addSubview:_text]; CGFloat W = frame.size.width, H = frame.size.height, s = 40, g = 6; // Landscape keeps the Dynamic Island on one side, which flips with rotation: inset both. CGFloat left = 56, right = W - 56; [self fx_button:@"Esc" key:MK_ESCAPE chars:nil frame:CGRectMake(left, 8, 46, 30)]; [self fx_button:@"Tab" key:MK_TAB chars:nil frame:CGRectMake(left + 52, 8, 46, 30)]; FXKeyButton *kb = [self fx_button:@"⌨︎" key:MK_NONE chars:nil frame:CGRectMake(right - 46, 8, 46, 30)]; [kb addTarget:self action:@selector(fx_toggle_keyboard) forControlEvents:UIControlEventTouchUpInside]; FXKeyButton *hide = [self fx_button:@"◐" key:MK_NONE chars:nil frame:CGRectMake(right - 46 - g - 40, 8, 40, 30)]; [hide addTarget:self action:@selector(fx_toggle_controls) forControlEvents:UIControlEventTouchUpInside]; FXStick *stick = [[FXStick alloc] initWithFrame:CGRectMake(left + 6, H - 128, 116, 116)]; [self addSubview:stick]; [_gameControls addObject:stick]; // Right-hand grid: common Factorio defaults. NSArray *grid = @[@[@"E", @(MK_E), @"e"], @[@"Q", @(MK_Q), @"q"], @[@"R", @(MK_R), @"r"], @[@"F", @(MK_F), @"f"], @[@"M", @(MK_M), @"m"], @[@"T", @(MK_T), @"t"], @[@"⇧", @(MK_LSHIFT), @""], @[@"Ctrl", @(MK_LCTRL), @""]]; for (NSUInteger i = 0; i < grid.count; i++) { NSUInteger col = i % 4, row = i / 4; CGRect f = CGRectMake(right - (4 - col) * (s + g) + g, H - 12 - (2 - row) * (s + g) + g, s, s); FXKeyButton *b = [self fx_button:grid[i][0] key:[grid[i][1] unsignedShortValue] chars:[grid[i][2] length] ? grid[i][2] : nil frame:f]; if (b.key == MK_LSHIFT || b.key == MK_LCTRL) b.sticky = YES; [_gameControls addObject:b]; } } return self; } - (void)fx_toggle_keyboard { if (_text.isFirstResponder) [_text resignFirstResponder]; else [_text becomeFirstResponder]; } - (void)fx_toggle_controls { BOOL hidden = !_gameControls.firstObject.hidden; for (UIView *v in _gameControls) v.hidden = hidden; } @end static FXControls *fx_controls; FX_EXPORT void fx_install_controls(UIView *root) { fx_controls = [[FXControls alloc] initWithFrame:root.bounds]; [root addSubview:fx_controls]; } FX_EXPORT void fx_controls_after_click(void) { fx_release_sticky(); }