summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/joystick/apple/SDL_mfijoystick.m
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
committer3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
commit5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch)
tree8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/src/joystick/apple/SDL_mfijoystick.m
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/src/joystick/apple/SDL_mfijoystick.m')
-rw-r--r--contrib/SDL-3.2.8/src/joystick/apple/SDL_mfijoystick.m1946
1 files changed, 1946 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/joystick/apple/SDL_mfijoystick.m b/contrib/SDL-3.2.8/src/joystick/apple/SDL_mfijoystick.m
new file mode 100644
index 0000000..811a9f1
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/joystick/apple/SDL_mfijoystick.m
@@ -0,0 +1,1946 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23// This is the iOS implementation of the SDL joystick API
24#include "../SDL_sysjoystick.h"
25#include "../SDL_joystick_c.h"
26#include "../hidapi/SDL_hidapijoystick_c.h"
27#include "../usb_ids.h"
28#include "../../events/SDL_events_c.h"
29
30#include "SDL_mfijoystick_c.h"
31
32
33#if defined(SDL_PLATFORM_IOS) && !defined(SDL_PLATFORM_TVOS)
34#import <CoreMotion/CoreMotion.h>
35#endif
36
37#ifdef SDL_PLATFORM_MACOS
38#include <IOKit/hid/IOHIDManager.h>
39#include <AppKit/NSApplication.h>
40#ifndef NSAppKitVersionNumber10_15
41#define NSAppKitVersionNumber10_15 1894
42#endif
43#endif // SDL_PLATFORM_MACOS
44
45#import <GameController/GameController.h>
46
47#ifdef SDL_JOYSTICK_MFI
48static id connectObserver = nil;
49static id disconnectObserver = nil;
50
51#include <objc/message.h>
52
53// Fix build errors when using an older SDK by defining these selectors
54@interface GCController (SDL)
55#if !((__IPHONE_OS_VERSION_MAX_ALLOWED >= 140500) || (__APPLETV_OS_VERSION_MAX_ALLOWED >= 140500) || (__MAC_OS_X_VERSION_MAX_ALLOWED >= 110300))
56@property(class, nonatomic, readwrite) BOOL shouldMonitorBackgroundEvents;
57#endif
58@end
59
60#import <CoreHaptics/CoreHaptics.h>
61
62#endif // SDL_JOYSTICK_MFI
63
64static SDL_JoystickDeviceItem *deviceList = NULL;
65
66static int numjoysticks = 0;
67int SDL_AppleTVRemoteOpenedAsJoystick = 0;
68
69static SDL_JoystickDeviceItem *GetDeviceForIndex(int device_index)
70{
71 SDL_JoystickDeviceItem *device = deviceList;
72 int i = 0;
73
74 while (i < device_index) {
75 if (device == NULL) {
76 return NULL;
77 }
78 device = device->next;
79 i++;
80 }
81
82 return device;
83}
84
85#ifdef SDL_JOYSTICK_MFI
86static bool IsControllerPS4(GCController *controller)
87{
88 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
89 if ([controller.productCategory isEqualToString:@"DualShock 4"]) {
90 return true;
91 }
92 } else {
93 if ([controller.vendorName containsString:@"DUALSHOCK"]) {
94 return true;
95 }
96 }
97 return false;
98}
99static bool IsControllerPS5(GCController *controller)
100{
101 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
102 if ([controller.productCategory isEqualToString:@"DualSense"]) {
103 return true;
104 }
105 } else {
106 if ([controller.vendorName containsString:@"DualSense"]) {
107 return true;
108 }
109 }
110 return false;
111}
112static bool IsControllerXbox(GCController *controller)
113{
114 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
115 if ([controller.productCategory isEqualToString:@"Xbox One"]) {
116 return true;
117 }
118 } else {
119 if ([controller.vendorName containsString:@"Xbox"]) {
120 return true;
121 }
122 }
123 return false;
124}
125static bool IsControllerSwitchPro(GCController *controller)
126{
127 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
128 if ([controller.productCategory isEqualToString:@"Switch Pro Controller"]) {
129 return true;
130 }
131 }
132 return false;
133}
134static bool IsControllerSwitchJoyConL(GCController *controller)
135{
136 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
137 if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (L)"]) {
138 return true;
139 }
140 }
141 return false;
142}
143static bool IsControllerSwitchJoyConR(GCController *controller)
144{
145 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
146 if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (R)"]) {
147 return true;
148 }
149 }
150 return false;
151}
152static bool IsControllerSwitchJoyConPair(GCController *controller)
153{
154 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
155 if ([controller.productCategory isEqualToString:@"Nintendo Switch Joy-Con (L/R)"]) {
156 return true;
157 }
158 }
159 return false;
160}
161static bool IsControllerStadia(GCController *controller)
162{
163 if ([controller.vendorName hasPrefix:@"Stadia"]) {
164 return true;
165 }
166 return false;
167}
168static bool IsControllerBackboneOne(GCController *controller)
169{
170 if ([controller.vendorName hasPrefix:@"Backbone One"]) {
171 return true;
172 }
173 return false;
174}
175static void CheckControllerSiriRemote(GCController *controller, int *is_siri_remote)
176{
177 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
178 if ([controller.productCategory hasPrefix:@"Siri Remote"]) {
179 *is_siri_remote = 1;
180 SDL_sscanf(controller.productCategory.UTF8String, "Siri Remote (%i%*s Generation)", is_siri_remote);
181 return;
182 }
183 }
184 *is_siri_remote = 0;
185}
186
187static bool ElementAlreadyHandled(SDL_JoystickDeviceItem *device, NSString *element, NSDictionary<NSString *, GCControllerElement *> *elements)
188{
189 if ([element isEqualToString:@"Left Thumbstick Left"] ||
190 [element isEqualToString:@"Left Thumbstick Right"]) {
191 if (elements[@"Left Thumbstick X Axis"]) {
192 return true;
193 }
194 }
195 if ([element isEqualToString:@"Left Thumbstick Up"] ||
196 [element isEqualToString:@"Left Thumbstick Down"]) {
197 if (elements[@"Left Thumbstick Y Axis"]) {
198 return true;
199 }
200 }
201 if ([element isEqualToString:@"Right Thumbstick Left"] ||
202 [element isEqualToString:@"Right Thumbstick Right"]) {
203 if (elements[@"Right Thumbstick X Axis"]) {
204 return true;
205 }
206 }
207 if ([element isEqualToString:@"Right Thumbstick Up"] ||
208 [element isEqualToString:@"Right Thumbstick Down"]) {
209 if (elements[@"Right Thumbstick Y Axis"]) {
210 return true;
211 }
212 }
213 if (device->is_siri_remote) {
214 if ([element isEqualToString:@"Direction Pad Left"] ||
215 [element isEqualToString:@"Direction Pad Right"]) {
216 if (elements[@"Direction Pad X Axis"]) {
217 return true;
218 }
219 }
220 if ([element isEqualToString:@"Direction Pad Up"] ||
221 [element isEqualToString:@"Direction Pad Down"]) {
222 if (elements[@"Direction Pad Y Axis"]) {
223 return true;
224 }
225 }
226 } else {
227 if ([element isEqualToString:@"Direction Pad X Axis"]) {
228 if (elements[@"Direction Pad Left"] &&
229 elements[@"Direction Pad Right"]) {
230 return true;
231 }
232 }
233 if ([element isEqualToString:@"Direction Pad Y Axis"]) {
234 if (elements[@"Direction Pad Up"] &&
235 elements[@"Direction Pad Down"]) {
236 return true;
237 }
238 }
239 }
240 if ([element isEqualToString:@"Cardinal Direction Pad X Axis"]) {
241 if (elements[@"Cardinal Direction Pad Left"] &&
242 elements[@"Cardinal Direction Pad Right"]) {
243 return true;
244 }
245 }
246 if ([element isEqualToString:@"Cardinal Direction Pad Y Axis"]) {
247 if (elements[@"Cardinal Direction Pad Up"] &&
248 elements[@"Cardinal Direction Pad Down"]) {
249 return true;
250 }
251 }
252 if ([element isEqualToString:@"Touchpad 1 X Axis"] ||
253 [element isEqualToString:@"Touchpad 1 Y Axis"] ||
254 [element isEqualToString:@"Touchpad 1 Left"] ||
255 [element isEqualToString:@"Touchpad 1 Right"] ||
256 [element isEqualToString:@"Touchpad 1 Up"] ||
257 [element isEqualToString:@"Touchpad 1 Down"] ||
258 [element isEqualToString:@"Touchpad 2 X Axis"] ||
259 [element isEqualToString:@"Touchpad 2 Y Axis"] ||
260 [element isEqualToString:@"Touchpad 2 Left"] ||
261 [element isEqualToString:@"Touchpad 2 Right"] ||
262 [element isEqualToString:@"Touchpad 2 Up"] ||
263 [element isEqualToString:@"Touchpad 2 Down"]) {
264 // The touchpad is handled separately
265 return true;
266 }
267 if ([element isEqualToString:@"Button Home"]) {
268 if (device->is_switch_joycon_pair) {
269 // The Nintendo Switch JoyCon home button doesn't ever show as being held down
270 return true;
271 }
272#ifdef SDL_PLATFORM_TVOS
273 // The OS uses the home button, it's not available to apps
274 return true;
275#endif
276 }
277 if ([element isEqualToString:@"Button Share"]) {
278 if (device->is_backbone_one) {
279 // The Backbone app uses share button
280 return true;
281 }
282 }
283 return false;
284}
285
286static bool IOS_AddMFIJoystickDevice(SDL_JoystickDeviceItem *device, GCController *controller)
287{
288 Uint16 vendor = 0;
289 Uint16 product = 0;
290 Uint8 subtype = 0;
291 const char *name = NULL;
292
293 if (@available(macOS 11.3, iOS 14.5, tvOS 14.5, *)) {
294 if (!GCController.shouldMonitorBackgroundEvents) {
295 GCController.shouldMonitorBackgroundEvents = YES;
296 }
297 }
298
299 /* Explicitly retain the controller because SDL_JoystickDeviceItem is a
300 * struct, and ARC doesn't work with structs. */
301 device->controller = (__bridge GCController *)CFBridgingRetain(controller);
302
303 if (controller.vendorName) {
304 name = controller.vendorName.UTF8String;
305 }
306
307 if (!name) {
308 name = "MFi Gamepad";
309 }
310
311 device->name = SDL_CreateJoystickName(0, 0, NULL, name);
312
313#ifdef DEBUG_CONTROLLER_PROFILE
314 NSLog(@"Product name: %@\n", controller.vendorName);
315 NSLog(@"Product category: %@\n", controller.productCategory);
316 NSLog(@"Elements available:\n");
317 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
318 NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
319 for (id key in controller.physicalInputProfile.buttons) {
320 NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital");
321 }
322 for (id key in controller.physicalInputProfile.axes) {
323 NSLog(@"\tAxis: %@\n", key);
324 }
325 for (id key in controller.physicalInputProfile.dpads) {
326 NSLog(@"\tHat: %@\n", key);
327 }
328 }
329#endif // DEBUG_CONTROLLER_PROFILE
330
331 device->is_xbox = IsControllerXbox(controller);
332 device->is_ps4 = IsControllerPS4(controller);
333 device->is_ps5 = IsControllerPS5(controller);
334 device->is_switch_pro = IsControllerSwitchPro(controller);
335 device->is_switch_joycon_pair = IsControllerSwitchJoyConPair(controller);
336 device->is_stadia = IsControllerStadia(controller);
337 device->is_backbone_one = IsControllerBackboneOne(controller);
338 device->is_switch_joyconL = IsControllerSwitchJoyConL(controller);
339 device->is_switch_joyconR = IsControllerSwitchJoyConR(controller);
340#ifdef SDL_JOYSTICK_HIDAPI
341 if ((device->is_xbox && (HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_XBOXONE) ||
342 HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_XBOX360))) ||
343 (device->is_ps4 && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_PS4)) ||
344 (device->is_ps5 && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_PS5)) ||
345 (device->is_switch_pro && HIDAPI_IsDeviceTypePresent(SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO)) ||
346 (device->is_switch_joycon_pair && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR, 0, "")) ||
347 (device->is_stadia && HIDAPI_IsDevicePresent(USB_VENDOR_GOOGLE, USB_PRODUCT_GOOGLE_STADIA_CONTROLLER, 0, "")) ||
348 (device->is_switch_joyconL && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT, 0, "")) ||
349 (device->is_switch_joyconR && HIDAPI_IsDevicePresent(USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT, 0, ""))) {
350 // The HIDAPI driver is taking care of this device
351 return false;
352 }
353#endif
354 if (device->is_xbox && SDL_strncmp(name, "GamePad-", 8) == 0) {
355 // This is a Steam Virtual Gamepad, which isn't supported by GCController
356 return false;
357 }
358 CheckControllerSiriRemote(controller, &device->is_siri_remote);
359
360 if (device->is_siri_remote && !SDL_GetHintBoolean(SDL_HINT_TV_REMOTE_AS_JOYSTICK, true)) {
361 // Ignore remotes, they'll be handled as keyboard input
362 return false;
363 }
364
365 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
366 if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton] != nil) {
367 device->has_dualshock_touchpad = TRUE;
368 }
369 if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne] != nil) {
370 device->has_xbox_paddles = TRUE;
371 }
372 if (controller.physicalInputProfile.buttons[@"Button Share"] != nil) {
373 device->has_xbox_share_button = TRUE;
374 }
375 }
376
377 if (device->is_backbone_one) {
378 vendor = USB_VENDOR_BACKBONE;
379 if (device->is_ps5) {
380 product = USB_PRODUCT_BACKBONE_ONE_IOS_PS5;
381 } else {
382 product = USB_PRODUCT_BACKBONE_ONE_IOS;
383 }
384 } else if (device->is_xbox) {
385 vendor = USB_VENDOR_MICROSOFT;
386 if (device->has_xbox_paddles) {
387 // Assume Xbox One Elite Series 2 Controller unless/until GCController flows VID/PID
388 product = USB_PRODUCT_XBOX_ONE_ELITE_SERIES_2_BLUETOOTH;
389 } else if (device->has_xbox_share_button) {
390 // Assume Xbox Series X Controller unless/until GCController flows VID/PID
391 product = USB_PRODUCT_XBOX_SERIES_X_BLE;
392 } else {
393 // Assume Xbox One S Bluetooth Controller unless/until GCController flows VID/PID
394 product = USB_PRODUCT_XBOX_ONE_S_REV1_BLUETOOTH;
395 }
396 } else if (device->is_ps4) {
397 // Assume DS4 Slim unless/until GCController flows VID/PID
398 vendor = USB_VENDOR_SONY;
399 product = USB_PRODUCT_SONY_DS4_SLIM;
400 if (device->has_dualshock_touchpad) {
401 subtype = 1;
402 }
403 } else if (device->is_ps5) {
404 vendor = USB_VENDOR_SONY;
405 product = USB_PRODUCT_SONY_DS5;
406 } else if (device->is_switch_pro) {
407 vendor = USB_VENDOR_NINTENDO;
408 product = USB_PRODUCT_NINTENDO_SWITCH_PRO;
409 device->has_nintendo_buttons = TRUE;
410 } else if (device->is_switch_joycon_pair) {
411 vendor = USB_VENDOR_NINTENDO;
412 product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR;
413 device->has_nintendo_buttons = TRUE;
414 } else if (device->is_switch_joyconL) {
415 vendor = USB_VENDOR_NINTENDO;
416 product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT;
417 } else if (device->is_switch_joyconR) {
418 vendor = USB_VENDOR_NINTENDO;
419 product = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT;
420 } else if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
421 vendor = USB_VENDOR_APPLE;
422 product = 4;
423 subtype = 4;
424 } else if (controller.extendedGamepad) {
425 vendor = USB_VENDOR_APPLE;
426 product = 1;
427 subtype = 1;
428#ifdef SDL_PLATFORM_TVOS
429 } else if (controller.microGamepad) {
430 vendor = USB_VENDOR_APPLE;
431 product = 3;
432 subtype = 3;
433#endif
434 } else {
435 // We don't know how to get input events from this device
436 return false;
437 }
438
439 if (SDL_ShouldIgnoreJoystick(vendor, product, 0, name)) {
440 return false;
441 }
442
443 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
444 NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
445
446 // Provide both axes and analog buttons as SDL axes
447 NSArray *axes = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]
448 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {
449 if (ElementAlreadyHandled(device, (NSString *)object, elements)) {
450 return false;
451 }
452
453 GCControllerElement *element = elements[object];
454 if (element.analog) {
455 if ([element isKindOfClass:[GCControllerAxisInput class]] ||
456 [element isKindOfClass:[GCControllerButtonInput class]]) {
457 return true;
458 }
459 }
460 return false;
461 }]];
462 NSArray *buttons = [[[elements allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]
463 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {
464 if (ElementAlreadyHandled(device, (NSString *)object, elements)) {
465 return false;
466 }
467
468 GCControllerElement *element = elements[object];
469 if ([element isKindOfClass:[GCControllerButtonInput class]]) {
470 return true;
471 }
472 return false;
473 }]];
474 /* Explicitly retain the arrays because SDL_JoystickDeviceItem is a
475 * struct, and ARC doesn't work with structs. */
476 device->naxes = (int)axes.count;
477 device->axes = (__bridge NSArray *)CFBridgingRetain(axes);
478 device->nbuttons = (int)buttons.count;
479 device->buttons = (__bridge NSArray *)CFBridgingRetain(buttons);
480 subtype = 4;
481
482#ifdef DEBUG_CONTROLLER_PROFILE
483 NSLog(@"Elements used:\n", controller.vendorName);
484 for (id key in device->buttons) {
485 NSLog(@"\tButton: %@ (%s)\n", key, elements[key].analog ? "analog" : "digital");
486 }
487 for (id key in device->axes) {
488 NSLog(@"\tAxis: %@\n", key);
489 }
490#endif // DEBUG_CONTROLLER_PROFILE
491
492#ifdef SDL_PLATFORM_TVOS
493 // tvOS turns the menu button into a system gesture, so we grab it here instead
494 if (elements[GCInputButtonMenu] && !elements[@"Button Home"]) {
495 device->pause_button_index = (int)[device->buttons indexOfObject:GCInputButtonMenu];
496 }
497#endif
498 } else if (controller.extendedGamepad) {
499 GCExtendedGamepad *gamepad = controller.extendedGamepad;
500 int nbuttons = 0;
501 BOOL has_direct_menu = FALSE;
502
503 // These buttons are part of the original MFi spec
504 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH);
505 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST);
506 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_WEST);
507 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_NORTH);
508 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_SHOULDER);
509 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
510 nbuttons += 6;
511
512 // These buttons are available on some newer controllers
513 if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) {
514 if (gamepad.leftThumbstickButton) {
515 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK);
516 ++nbuttons;
517 }
518 if (gamepad.rightThumbstickButton) {
519 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK);
520 ++nbuttons;
521 }
522 }
523 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
524 if (gamepad.buttonOptions) {
525 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_BACK);
526 ++nbuttons;
527 }
528 }
529 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_START);
530 ++nbuttons;
531
532 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
533 if (gamepad.buttonMenu) {
534 has_direct_menu = TRUE;
535 }
536 }
537#ifdef SDL_PLATFORM_TVOS
538 // The single menu button isn't very reliable, at least as of tvOS 16.1
539 if ((device->button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) == 0) {
540 has_direct_menu = FALSE;
541 }
542#endif
543 if (!has_direct_menu) {
544 device->pause_button_index = (nbuttons - 1);
545 }
546
547 device->naxes = 6; // 2 thumbsticks and 2 triggers
548 device->nhats = 1; // d-pad
549 device->nbuttons = nbuttons;
550 }
551#ifdef SDL_PLATFORM_TVOS
552 else if (controller.microGamepad) {
553 int nbuttons = 0;
554
555 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_SOUTH);
556 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_WEST); // Button X on microGamepad
557 device->button_mask |= (1 << SDL_GAMEPAD_BUTTON_EAST);
558 nbuttons += 3;
559 device->pause_button_index = (nbuttons - 1);
560
561 device->naxes = 2; // treat the touch surface as two axes
562 device->nhats = 0; // apparently the touch surface-as-dpad is buggy
563 device->nbuttons = nbuttons;
564
565 controller.microGamepad.allowsRotation = SDL_GetHintBoolean(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION, false);
566 }
567#endif
568 else {
569 // We don't know how to get input events from this device
570 return false;
571 }
572
573 Uint16 signature;
574 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
575 signature = 0;
576 signature = SDL_crc16(signature, device->name, SDL_strlen(device->name));
577 for (id key in device->axes) {
578 const char *string = ((NSString *)key).UTF8String;
579 signature = SDL_crc16(signature, string, SDL_strlen(string));
580 }
581 for (id key in device->buttons) {
582 const char *string = ((NSString *)key).UTF8String;
583 signature = SDL_crc16(signature, string, SDL_strlen(string));
584 }
585 } else {
586 signature = device->button_mask;
587 }
588 device->guid = SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, vendor, product, signature, NULL, name, 'm', subtype);
589
590 /* This will be set when the first button press of the controller is
591 * detected. */
592 controller.playerIndex = -1;
593 return true;
594}
595#endif // SDL_JOYSTICK_MFI
596
597#ifdef SDL_JOYSTICK_MFI
598static void IOS_AddJoystickDevice(GCController *controller)
599{
600 SDL_JoystickDeviceItem *device = deviceList;
601
602 while (device != NULL) {
603 if (device->controller == controller) {
604 return;
605 }
606 device = device->next;
607 }
608
609 device = (SDL_JoystickDeviceItem *)SDL_calloc(1, sizeof(SDL_JoystickDeviceItem));
610 if (device == NULL) {
611 return;
612 }
613
614 device->instance_id = SDL_GetNextObjectID();
615 device->pause_button_index = -1;
616
617 if (controller) {
618#ifdef SDL_JOYSTICK_MFI
619 if (!IOS_AddMFIJoystickDevice(device, controller)) {
620 SDL_free(device->name);
621 SDL_free(device);
622 return;
623 }
624#else
625 SDL_free(device);
626 return;
627#endif // SDL_JOYSTICK_MFI
628 }
629
630 if (deviceList == NULL) {
631 deviceList = device;
632 } else {
633 SDL_JoystickDeviceItem *lastdevice = deviceList;
634 while (lastdevice->next != NULL) {
635 lastdevice = lastdevice->next;
636 }
637 lastdevice->next = device;
638 }
639
640 ++numjoysticks;
641
642 SDL_PrivateJoystickAdded(device->instance_id);
643}
644#endif // SDL_JOYSTICK_MFI
645
646static SDL_JoystickDeviceItem *IOS_RemoveJoystickDevice(SDL_JoystickDeviceItem *device)
647{
648 SDL_JoystickDeviceItem *prev = NULL;
649 SDL_JoystickDeviceItem *next = NULL;
650 SDL_JoystickDeviceItem *item = deviceList;
651
652 if (device == NULL) {
653 return NULL;
654 }
655
656 next = device->next;
657
658 while (item != NULL) {
659 if (item == device) {
660 break;
661 }
662 prev = item;
663 item = item->next;
664 }
665
666 // Unlink the device item from the device list.
667 if (prev) {
668 prev->next = device->next;
669 } else if (device == deviceList) {
670 deviceList = device->next;
671 }
672
673 if (device->joystick) {
674 device->joystick->hwdata = NULL;
675 }
676
677#ifdef SDL_JOYSTICK_MFI
678 @autoreleasepool {
679 // These were explicitly retained in the struct, so they should be explicitly released before freeing the struct.
680 if (device->controller) {
681 GCController *controller = CFBridgingRelease((__bridge CFTypeRef)(device->controller));
682 controller.controllerPausedHandler = nil;
683 device->controller = nil;
684 }
685 if (device->axes) {
686 CFRelease((__bridge CFTypeRef)device->axes);
687 device->axes = nil;
688 }
689 if (device->buttons) {
690 CFRelease((__bridge CFTypeRef)device->buttons);
691 device->buttons = nil;
692 }
693 }
694#endif // SDL_JOYSTICK_MFI
695
696 --numjoysticks;
697
698 SDL_PrivateJoystickRemoved(device->instance_id);
699
700 SDL_free(device->name);
701 SDL_free(device);
702
703 return next;
704}
705
706#ifdef SDL_PLATFORM_TVOS
707static void SDLCALL SDL_AppleTVRemoteRotationHintChanged(void *udata, const char *name, const char *oldValue, const char *newValue)
708{
709 BOOL allowRotation = newValue != NULL && *newValue != '0';
710
711 @autoreleasepool {
712 for (GCController *controller in [GCController controllers]) {
713 if (controller.microGamepad) {
714 controller.microGamepad.allowsRotation = allowRotation;
715 }
716 }
717 }
718}
719#endif // SDL_PLATFORM_TVOS
720
721static bool IOS_JoystickInit(void)
722{
723 if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_MFI, true)) {
724 return true;
725 }
726
727#ifdef SDL_PLATFORM_MACOS
728 if (@available(macOS 10.16, *)) {
729 // Continue with initialization on macOS 11+
730 } else {
731 return true;
732 }
733#endif
734
735 @autoreleasepool {
736#ifdef SDL_JOYSTICK_MFI
737 NSNotificationCenter *center;
738#endif
739
740#ifdef SDL_JOYSTICK_MFI
741 // GameController.framework was added in iOS 7.
742 if (![GCController class]) {
743 return true;
744 }
745
746 /* For whatever reason, this always returns an empty array on
747 macOS 11.0.1 */
748 for (GCController *controller in [GCController controllers]) {
749 IOS_AddJoystickDevice(controller);
750 }
751
752#ifdef SDL_PLATFORM_TVOS
753 SDL_AddHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION,
754 SDL_AppleTVRemoteRotationHintChanged, NULL);
755#endif // SDL_PLATFORM_TVOS
756
757 center = [NSNotificationCenter defaultCenter];
758
759 connectObserver = [center addObserverForName:GCControllerDidConnectNotification
760 object:nil
761 queue:nil
762 usingBlock:^(NSNotification *note) {
763 GCController *controller = note.object;
764 SDL_LockJoysticks();
765 IOS_AddJoystickDevice(controller);
766 SDL_UnlockJoysticks();
767 }];
768
769 disconnectObserver = [center addObserverForName:GCControllerDidDisconnectNotification
770 object:nil
771 queue:nil
772 usingBlock:^(NSNotification *note) {
773 GCController *controller = note.object;
774 SDL_JoystickDeviceItem *device;
775 SDL_LockJoysticks();
776 for (device = deviceList; device != NULL; device = device->next) {
777 if (device->controller == controller) {
778 IOS_RemoveJoystickDevice(device);
779 break;
780 }
781 }
782 SDL_UnlockJoysticks();
783 }];
784#endif // SDL_JOYSTICK_MFI
785 }
786
787 return true;
788}
789
790static int IOS_JoystickGetCount(void)
791{
792 return numjoysticks;
793}
794
795static void IOS_JoystickDetect(void)
796{
797}
798
799static bool IOS_JoystickIsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name)
800{
801 // We don't override any other drivers through this method
802 return false;
803}
804
805static const char *IOS_JoystickGetDeviceName(int device_index)
806{
807 SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
808 return device ? device->name : "Unknown";
809}
810
811static const char *IOS_JoystickGetDevicePath(int device_index)
812{
813 return NULL;
814}
815
816static int IOS_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index)
817{
818 return -1;
819}
820
821static int IOS_JoystickGetDevicePlayerIndex(int device_index)
822{
823#ifdef SDL_JOYSTICK_MFI
824 SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
825 if (device && device->controller) {
826 return (int)device->controller.playerIndex;
827 }
828#endif
829 return -1;
830}
831
832static void IOS_JoystickSetDevicePlayerIndex(int device_index, int player_index)
833{
834#ifdef SDL_JOYSTICK_MFI
835 SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
836 if (device && device->controller) {
837 device->controller.playerIndex = player_index;
838 }
839#endif
840}
841
842static SDL_GUID IOS_JoystickGetDeviceGUID(int device_index)
843{
844 SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
845 SDL_GUID guid;
846 if (device) {
847 guid = device->guid;
848 } else {
849 SDL_zero(guid);
850 }
851 return guid;
852}
853
854static SDL_JoystickID IOS_JoystickGetDeviceInstanceID(int device_index)
855{
856 SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
857 return device ? device->instance_id : 0;
858}
859
860static bool IOS_JoystickOpen(SDL_Joystick *joystick, int device_index)
861{
862 SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
863 if (device == NULL) {
864 return SDL_SetError("Could not open Joystick: no hardware device for the specified index");
865 }
866
867 joystick->hwdata = device;
868
869 joystick->naxes = device->naxes;
870 joystick->nhats = device->nhats;
871 joystick->nbuttons = device->nbuttons;
872
873 if (device->has_dualshock_touchpad) {
874 SDL_PrivateJoystickAddTouchpad(joystick, 2);
875 }
876
877 device->joystick = joystick;
878
879 @autoreleasepool {
880#ifdef SDL_JOYSTICK_MFI
881 if (device->pause_button_index >= 0) {
882 GCController *controller = device->controller;
883 controller.controllerPausedHandler = ^(GCController *c) {
884 if (joystick->hwdata) {
885 joystick->hwdata->pause_button_pressed = SDL_GetTicks();
886 }
887 };
888 }
889
890 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
891 GCController *controller = joystick->hwdata->controller;
892 GCMotion *motion = controller.motion;
893 if (motion && motion.hasRotationRate) {
894 SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 0.0f);
895 }
896 if (motion && motion.hasGravityAndUserAcceleration) {
897 SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 0.0f);
898 }
899 }
900
901 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
902 GCController *controller = joystick->hwdata->controller;
903 for (id key in controller.physicalInputProfile.buttons) {
904 GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];
905 if ([button isBoundToSystemGesture]) {
906 button.preferredSystemGestureState = GCSystemGestureStateDisabled;
907 }
908 }
909 }
910
911 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
912 GCController *controller = device->controller;
913 if (controller.light) {
914 SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RGB_LED_BOOLEAN, true);
915 }
916
917 if (controller.haptics) {
918 for (GCHapticsLocality locality in controller.haptics.supportedLocalities) {
919 if ([locality isEqualToString:GCHapticsLocalityHandles]) {
920 SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, true);
921 } else if ([locality isEqualToString:GCHapticsLocalityTriggers]) {
922 SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_TRIGGER_RUMBLE_BOOLEAN, true);
923 }
924 }
925 }
926 }
927#endif // SDL_JOYSTICK_MFI
928 }
929 if (device->is_siri_remote) {
930 ++SDL_AppleTVRemoteOpenedAsJoystick;
931 }
932
933 return true;
934}
935
936#ifdef SDL_JOYSTICK_MFI
937static Uint8 IOS_MFIJoystickHatStateForDPad(GCControllerDirectionPad *dpad)
938{
939 Uint8 hat = 0;
940
941 if (dpad.up.isPressed) {
942 hat |= SDL_HAT_UP;
943 } else if (dpad.down.isPressed) {
944 hat |= SDL_HAT_DOWN;
945 }
946
947 if (dpad.left.isPressed) {
948 hat |= SDL_HAT_LEFT;
949 } else if (dpad.right.isPressed) {
950 hat |= SDL_HAT_RIGHT;
951 }
952
953 if (hat == 0) {
954 return SDL_HAT_CENTERED;
955 }
956
957 return hat;
958}
959#endif
960
961static void IOS_MFIJoystickUpdate(SDL_Joystick *joystick)
962{
963#ifdef SDL_JOYSTICK_MFI
964 @autoreleasepool {
965 SDL_JoystickDeviceItem *device = joystick->hwdata;
966 GCController *controller = device->controller;
967 Uint8 hatstate = SDL_HAT_CENTERED;
968 int i;
969 Uint64 timestamp = SDL_GetTicksNS();
970
971#ifdef DEBUG_CONTROLLER_STATE
972 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
973 if (controller.physicalInputProfile) {
974 for (id key in controller.physicalInputProfile.buttons) {
975 GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];
976 if (button.isPressed)
977 NSLog(@"Button %@ = %s\n", key, button.isPressed ? "pressed" : "released");
978 }
979 for (id key in controller.physicalInputProfile.axes) {
980 GCControllerAxisInput *axis = controller.physicalInputProfile.axes[key];
981 if (axis.value != 0.0f)
982 NSLog(@"Axis %@ = %g\n", key, axis.value);
983 }
984 for (id key in controller.physicalInputProfile.dpads) {
985 GCControllerDirectionPad *dpad = controller.physicalInputProfile.dpads[key];
986 if (dpad.up.isPressed || dpad.down.isPressed || dpad.left.isPressed || dpad.right.isPressed) {
987 NSLog(@"Hat %@ =%s%s%s%s\n", key,
988 dpad.up.isPressed ? " UP" : "",
989 dpad.down.isPressed ? " DOWN" : "",
990 dpad.left.isPressed ? " LEFT" : "",
991 dpad.right.isPressed ? " RIGHT" : "");
992 }
993 }
994 }
995 }
996#endif // DEBUG_CONTROLLER_STATE
997
998 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
999 NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
1000 NSDictionary<NSString *, GCControllerButtonInput *> *buttons = controller.physicalInputProfile.buttons;
1001
1002 int axis = 0;
1003 for (id key in device->axes) {
1004 Sint16 value;
1005 GCControllerElement *element = elements[key];
1006 if ([element isKindOfClass:[GCControllerAxisInput class]]) {
1007 value = (Sint16)([(GCControllerAxisInput *)element value] * 32767);
1008 } else {
1009 value = (Sint16)([(GCControllerButtonInput *)element value] * 32767);
1010 }
1011 SDL_SendJoystickAxis(timestamp, joystick, axis++, value);
1012 }
1013
1014 int button = 0;
1015 for (id key in device->buttons) {
1016 bool down;
1017 if (button == device->pause_button_index) {
1018 down = (device->pause_button_pressed > 0);
1019 } else {
1020 down = buttons[key].isPressed;
1021 }
1022 SDL_SendJoystickButton(timestamp, joystick, button++, down);
1023 }
1024 } else if (controller.extendedGamepad) {
1025 bool isstack;
1026 GCExtendedGamepad *gamepad = controller.extendedGamepad;
1027
1028 // Axis order matches the XInput Windows mappings.
1029 Sint16 axes[] = {
1030 (Sint16)(gamepad.leftThumbstick.xAxis.value * 32767),
1031 (Sint16)(gamepad.leftThumbstick.yAxis.value * -32767),
1032 (Sint16)((gamepad.leftTrigger.value * 65535) - 32768),
1033 (Sint16)(gamepad.rightThumbstick.xAxis.value * 32767),
1034 (Sint16)(gamepad.rightThumbstick.yAxis.value * -32767),
1035 (Sint16)((gamepad.rightTrigger.value * 65535) - 32768),
1036 };
1037
1038 // Button order matches the XInput Windows mappings.
1039 bool *buttons = SDL_small_alloc(bool, joystick->nbuttons, &isstack);
1040 int button_count = 0;
1041
1042 if (buttons == NULL) {
1043 return;
1044 }
1045
1046 // These buttons are part of the original MFi spec
1047 buttons[button_count++] = gamepad.buttonA.isPressed;
1048 buttons[button_count++] = gamepad.buttonB.isPressed;
1049 buttons[button_count++] = gamepad.buttonX.isPressed;
1050 buttons[button_count++] = gamepad.buttonY.isPressed;
1051 buttons[button_count++] = gamepad.leftShoulder.isPressed;
1052 buttons[button_count++] = gamepad.rightShoulder.isPressed;
1053
1054 // These buttons are available on some newer controllers
1055 if (@available(macOS 10.14.1, iOS 12.1, tvOS 12.1, *)) {
1056 if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_LEFT_STICK)) {
1057 buttons[button_count++] = gamepad.leftThumbstickButton.isPressed;
1058 }
1059 if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_RIGHT_STICK)) {
1060 buttons[button_count++] = gamepad.rightThumbstickButton.isPressed;
1061 }
1062 }
1063 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
1064 if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_BACK)) {
1065 buttons[button_count++] = gamepad.buttonOptions.isPressed;
1066 }
1067 }
1068 if (device->button_mask & (1 << SDL_GAMEPAD_BUTTON_START)) {
1069 if (device->pause_button_index >= 0) {
1070 // Guaranteed if buttonMenu is not supported on this OS
1071 buttons[button_count++] = (device->pause_button_pressed > 0);
1072 } else {
1073 if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) {
1074 buttons[button_count++] = gamepad.buttonMenu.isPressed;
1075 }
1076 }
1077 }
1078
1079 hatstate = IOS_MFIJoystickHatStateForDPad(gamepad.dpad);
1080
1081 for (i = 0; i < SDL_arraysize(axes); i++) {
1082 SDL_SendJoystickAxis(timestamp, joystick, i, axes[i]);
1083 }
1084
1085 for (i = 0; i < button_count; i++) {
1086 SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]);
1087 }
1088
1089 SDL_small_free(buttons, isstack);
1090 }
1091#ifdef SDL_PLATFORM_TVOS
1092 else if (controller.microGamepad) {
1093 GCMicroGamepad *gamepad = controller.microGamepad;
1094
1095 Sint16 axes[] = {
1096 (Sint16)(gamepad.dpad.xAxis.value * 32767),
1097 (Sint16)(gamepad.dpad.yAxis.value * -32767),
1098 };
1099
1100 for (i = 0; i < SDL_arraysize(axes); i++) {
1101 SDL_SendJoystickAxis(timestamp, joystick, i, axes[i]);
1102 }
1103
1104 bool buttons[joystick->nbuttons];
1105 int button_count = 0;
1106 buttons[button_count++] = gamepad.buttonA.isPressed;
1107 buttons[button_count++] = gamepad.buttonX.isPressed;
1108 buttons[button_count++] = (device->pause_button_pressed > 0);
1109
1110 for (i = 0; i < button_count; i++) {
1111 SDL_SendJoystickButton(timestamp, joystick, i, buttons[i]);
1112 }
1113 }
1114#endif // SDL_PLATFORM_TVOS
1115
1116 if (joystick->nhats > 0) {
1117 SDL_SendJoystickHat(timestamp, joystick, 0, hatstate);
1118 }
1119
1120 if (device->pause_button_pressed) {
1121 // The pause callback is instantaneous, so we extend the duration to allow "holding down" by pressing it repeatedly
1122 const int PAUSE_BUTTON_PRESS_DURATION_MS = 250;
1123 if (SDL_GetTicks() >= device->pause_button_pressed + PAUSE_BUTTON_PRESS_DURATION_MS) {
1124 device->pause_button_pressed = 0;
1125 }
1126 }
1127
1128 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1129 if (device->has_dualshock_touchpad) {
1130 GCControllerDirectionPad *dpad;
1131
1132 dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne];
1133 if (dpad.xAxis.value != 0.f || dpad.yAxis.value != 0.f) {
1134 SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, true, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f);
1135 } else {
1136 SDL_SendJoystickTouchpad(timestamp, joystick, 0, 0, false, 0.0f, 0.0f, 1.0f);
1137 }
1138
1139 dpad = controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo];
1140 if (dpad.xAxis.value != 0.f || dpad.yAxis.value != 0.f) {
1141 SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, true, (1.0f + dpad.xAxis.value) * 0.5f, 1.0f - (1.0f + dpad.yAxis.value) * 0.5f, 1.0f);
1142 } else {
1143 SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, false, 0.0f, 0.0f, 1.0f);
1144 }
1145 }
1146 }
1147
1148 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1149 GCMotion *motion = controller.motion;
1150 if (motion && motion.sensorsActive) {
1151 float data[3];
1152
1153 if (motion.hasRotationRate) {
1154 GCRotationRate rate = motion.rotationRate;
1155 data[0] = rate.x;
1156 data[1] = rate.z;
1157 data[2] = -rate.y;
1158 SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, timestamp, data, 3);
1159 }
1160 if (motion.hasGravityAndUserAcceleration) {
1161 GCAcceleration accel = motion.acceleration;
1162 data[0] = -accel.x * SDL_STANDARD_GRAVITY;
1163 data[1] = -accel.y * SDL_STANDARD_GRAVITY;
1164 data[2] = -accel.z * SDL_STANDARD_GRAVITY;
1165 SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, timestamp, data, 3);
1166 }
1167 }
1168 }
1169
1170 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1171 GCDeviceBattery *battery = controller.battery;
1172 if (battery) {
1173 SDL_PowerState state = SDL_POWERSTATE_UNKNOWN;
1174 int percent = (int)SDL_roundf(battery.batteryLevel * 100.0f);
1175
1176 switch (battery.batteryState) {
1177 case GCDeviceBatteryStateDischarging:
1178 state = SDL_POWERSTATE_ON_BATTERY;
1179 break;
1180 case GCDeviceBatteryStateCharging:
1181 state = SDL_POWERSTATE_CHARGING;
1182 break;
1183 case GCDeviceBatteryStateFull:
1184 state = SDL_POWERSTATE_CHARGED;
1185 break;
1186 default:
1187 break;
1188 }
1189
1190 SDL_SendJoystickPowerInfo(joystick, state, percent);
1191 }
1192 }
1193 }
1194#endif // SDL_JOYSTICK_MFI
1195}
1196
1197#ifdef SDL_JOYSTICK_MFI
1198@interface SDL3_RumbleMotor : NSObject
1199@property(nonatomic, strong) CHHapticEngine *engine API_AVAILABLE(macos(10.16), ios(13.0), tvos(14.0));
1200@property(nonatomic, strong) id<CHHapticPatternPlayer> player API_AVAILABLE(macos(10.16), ios(13.0), tvos(14.0));
1201@property bool active;
1202@end
1203
1204@implementation SDL3_RumbleMotor
1205{
1206}
1207
1208- (void)cleanup
1209{
1210 @autoreleasepool {
1211 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1212 if (self.player != nil) {
1213 [self.player cancelAndReturnError:nil];
1214 self.player = nil;
1215 }
1216 if (self.engine != nil) {
1217 [self.engine stopWithCompletionHandler:nil];
1218 self.engine = nil;
1219 }
1220 }
1221 }
1222}
1223
1224- (bool)setIntensity:(float)intensity
1225{
1226 @autoreleasepool {
1227 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1228 NSError *error = nil;
1229 CHHapticDynamicParameter *param;
1230
1231 if (self.engine == nil) {
1232 return SDL_SetError("Haptics engine was stopped");
1233 }
1234
1235 if (intensity == 0.0f) {
1236 if (self.player && self.active) {
1237 [self.player stopAtTime:0 error:&error];
1238 }
1239 self.active = false;
1240 return true;
1241 }
1242
1243 if (self.player == nil) {
1244 CHHapticEventParameter *event_param = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
1245 CHHapticEvent *event = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObjects:event_param, nil] relativeTime:0 duration:GCHapticDurationInfinite];
1246 CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:event] parameters:[[NSArray alloc] init] error:&error];
1247 if (error != nil) {
1248 return SDL_SetError("Couldn't create haptic pattern: %s", [error.localizedDescription UTF8String]);
1249 }
1250
1251 self.player = [self.engine createPlayerWithPattern:pattern error:&error];
1252 if (error != nil) {
1253 return SDL_SetError("Couldn't create haptic player: %s", [error.localizedDescription UTF8String]);
1254 }
1255 self.active = false;
1256 }
1257
1258 param = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:intensity relativeTime:0];
1259 [self.player sendParameters:[NSArray arrayWithObject:param] atTime:0 error:&error];
1260 if (error != nil) {
1261 return SDL_SetError("Couldn't update haptic player: %s", [error.localizedDescription UTF8String]);
1262 }
1263
1264 if (!self.active) {
1265 [self.player startAtTime:0 error:&error];
1266 self.active = true;
1267 }
1268 }
1269
1270 return true;
1271 }
1272}
1273
1274- (id)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality API_AVAILABLE(macos(10.16), ios(14.0), tvos(14.0))
1275{
1276 @autoreleasepool {
1277 NSError *error;
1278 __weak __typeof(self) weakSelf;
1279 self = [super init];
1280 weakSelf = self;
1281
1282 self.engine = [controller.haptics createEngineWithLocality:locality];
1283 if (self.engine == nil) {
1284 SDL_SetError("Couldn't create haptics engine");
1285 return nil;
1286 }
1287
1288 [self.engine startAndReturnError:&error];
1289 if (error != nil) {
1290 SDL_SetError("Couldn't start haptics engine");
1291 return nil;
1292 }
1293
1294 self.engine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
1295 SDL3_RumbleMotor *_this = weakSelf;
1296 if (_this == nil) {
1297 return;
1298 }
1299
1300 _this.player = nil;
1301 _this.engine = nil;
1302 };
1303 self.engine.resetHandler = ^{
1304 SDL3_RumbleMotor *_this = weakSelf;
1305 if (_this == nil) {
1306 return;
1307 }
1308
1309 _this.player = nil;
1310 [_this.engine startAndReturnError:nil];
1311 };
1312
1313 return self;
1314 }
1315}
1316
1317@end
1318
1319@interface SDL3_RumbleContext : NSObject
1320@property(nonatomic, strong) SDL3_RumbleMotor *lowFrequencyMotor;
1321@property(nonatomic, strong) SDL3_RumbleMotor *highFrequencyMotor;
1322@property(nonatomic, strong) SDL3_RumbleMotor *leftTriggerMotor;
1323@property(nonatomic, strong) SDL3_RumbleMotor *rightTriggerMotor;
1324@end
1325
1326@implementation SDL3_RumbleContext
1327{
1328}
1329
1330- (id)initWithLowFrequencyMotor:(SDL3_RumbleMotor *)low_frequency_motor
1331 HighFrequencyMotor:(SDL3_RumbleMotor *)high_frequency_motor
1332 LeftTriggerMotor:(SDL3_RumbleMotor *)left_trigger_motor
1333 RightTriggerMotor:(SDL3_RumbleMotor *)right_trigger_motor
1334{
1335 self = [super init];
1336 self.lowFrequencyMotor = low_frequency_motor;
1337 self.highFrequencyMotor = high_frequency_motor;
1338 self.leftTriggerMotor = left_trigger_motor;
1339 self.rightTriggerMotor = right_trigger_motor;
1340 return self;
1341}
1342
1343- (bool)rumbleWithLowFrequency:(Uint16)low_frequency_rumble andHighFrequency:(Uint16)high_frequency_rumble
1344{
1345 bool result = true;
1346
1347 result &= [self.lowFrequencyMotor setIntensity:((float)low_frequency_rumble / 65535.0f)];
1348 result &= [self.highFrequencyMotor setIntensity:((float)high_frequency_rumble / 65535.0f)];
1349 return result;
1350}
1351
1352- (bool)rumbleLeftTrigger:(Uint16)left_rumble andRightTrigger:(Uint16)right_rumble
1353{
1354 bool result = false;
1355
1356 if (self.leftTriggerMotor && self.rightTriggerMotor) {
1357 result &= [self.leftTriggerMotor setIntensity:((float)left_rumble / 65535.0f)];
1358 result &= [self.rightTriggerMotor setIntensity:((float)right_rumble / 65535.0f)];
1359 } else {
1360 result = SDL_Unsupported();
1361 }
1362 return result;
1363}
1364
1365- (void)cleanup
1366{
1367 [self.lowFrequencyMotor cleanup];
1368 [self.highFrequencyMotor cleanup];
1369}
1370
1371@end
1372
1373static SDL3_RumbleContext *IOS_JoystickInitRumble(GCController *controller)
1374{
1375 @autoreleasepool {
1376 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1377 SDL3_RumbleMotor *low_frequency_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle];
1378 SDL3_RumbleMotor *high_frequency_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle];
1379 SDL3_RumbleMotor *left_trigger_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftTrigger];
1380 SDL3_RumbleMotor *right_trigger_motor = [[SDL3_RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightTrigger];
1381 if (low_frequency_motor && high_frequency_motor) {
1382 return [[SDL3_RumbleContext alloc] initWithLowFrequencyMotor:low_frequency_motor
1383 HighFrequencyMotor:high_frequency_motor
1384 LeftTriggerMotor:left_trigger_motor
1385 RightTriggerMotor:right_trigger_motor];
1386 }
1387 }
1388 }
1389 return nil;
1390}
1391
1392#endif // SDL_JOYSTICK_MFI
1393
1394static bool IOS_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
1395{
1396#ifdef SDL_JOYSTICK_MFI
1397 SDL_JoystickDeviceItem *device = joystick->hwdata;
1398
1399 if (device == NULL) {
1400 return SDL_SetError("Controller is no longer connected");
1401 }
1402
1403 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1404 if (!device->rumble && device->controller && device->controller.haptics) {
1405 SDL3_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller);
1406 if (rumble) {
1407 device->rumble = (void *)CFBridgingRetain(rumble);
1408 }
1409 }
1410 }
1411
1412 if (device->rumble) {
1413 SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;
1414 return [rumble rumbleWithLowFrequency:low_frequency_rumble andHighFrequency:high_frequency_rumble];
1415 }
1416#endif
1417 return SDL_Unsupported();
1418}
1419
1420static bool IOS_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
1421{
1422#ifdef SDL_JOYSTICK_MFI
1423 SDL_JoystickDeviceItem *device = joystick->hwdata;
1424
1425 if (device == NULL) {
1426 return SDL_SetError("Controller is no longer connected");
1427 }
1428
1429 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1430 if (!device->rumble && device->controller && device->controller.haptics) {
1431 SDL3_RumbleContext *rumble = IOS_JoystickInitRumble(device->controller);
1432 if (rumble) {
1433 device->rumble = (void *)CFBridgingRetain(rumble);
1434 }
1435 }
1436 }
1437
1438 if (device->rumble) {
1439 SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;
1440 return [rumble rumbleLeftTrigger:left_rumble andRightTrigger:right_rumble];
1441 }
1442#endif
1443 return SDL_Unsupported();
1444}
1445
1446static bool IOS_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
1447{
1448 @autoreleasepool {
1449 SDL_JoystickDeviceItem *device = joystick->hwdata;
1450
1451 if (device == NULL) {
1452 return SDL_SetError("Controller is no longer connected");
1453 }
1454
1455 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1456 GCController *controller = device->controller;
1457 GCDeviceLight *light = controller.light;
1458 if (light) {
1459 light.color = [[GCColor alloc] initWithRed:(float)red / 255.0f
1460 green:(float)green / 255.0f
1461 blue:(float)blue / 255.0f];
1462 return true;
1463 }
1464 }
1465 }
1466 return SDL_Unsupported();
1467}
1468
1469static bool IOS_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size)
1470{
1471 return SDL_Unsupported();
1472}
1473
1474static bool IOS_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled)
1475{
1476 @autoreleasepool {
1477 SDL_JoystickDeviceItem *device = joystick->hwdata;
1478
1479 if (device == NULL) {
1480 return SDL_SetError("Controller is no longer connected");
1481 }
1482
1483 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1484 GCController *controller = device->controller;
1485 GCMotion *motion = controller.motion;
1486 if (motion) {
1487 motion.sensorsActive = enabled ? YES : NO;
1488 return true;
1489 }
1490 }
1491 }
1492
1493 return SDL_Unsupported();
1494}
1495
1496static void IOS_JoystickUpdate(SDL_Joystick *joystick)
1497{
1498 SDL_JoystickDeviceItem *device = joystick->hwdata;
1499
1500 if (device == NULL) {
1501 return;
1502 }
1503
1504 if (device->controller) {
1505 IOS_MFIJoystickUpdate(joystick);
1506 }
1507}
1508
1509static void IOS_JoystickClose(SDL_Joystick *joystick)
1510{
1511 SDL_JoystickDeviceItem *device = joystick->hwdata;
1512
1513 if (device == NULL) {
1514 return;
1515 }
1516
1517 device->joystick = NULL;
1518
1519#ifdef SDL_JOYSTICK_MFI
1520 @autoreleasepool {
1521 if (device->rumble) {
1522 SDL3_RumbleContext *rumble = (__bridge SDL3_RumbleContext *)device->rumble;
1523
1524 [rumble cleanup];
1525 CFRelease(device->rumble);
1526 device->rumble = NULL;
1527 }
1528
1529 if (device->controller) {
1530 GCController *controller = device->controller;
1531 controller.controllerPausedHandler = nil;
1532 controller.playerIndex = -1;
1533
1534 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1535 for (id key in controller.physicalInputProfile.buttons) {
1536 GCControllerButtonInput *button = controller.physicalInputProfile.buttons[key];
1537 if ([button isBoundToSystemGesture]) {
1538 button.preferredSystemGestureState = GCSystemGestureStateEnabled;
1539 }
1540 }
1541 }
1542 }
1543 }
1544#endif // SDL_JOYSTICK_MFI
1545
1546 if (device->is_siri_remote) {
1547 --SDL_AppleTVRemoteOpenedAsJoystick;
1548 }
1549}
1550
1551static void IOS_JoystickQuit(void)
1552{
1553 @autoreleasepool {
1554#ifdef SDL_JOYSTICK_MFI
1555 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
1556
1557 if (connectObserver) {
1558 [center removeObserver:connectObserver name:GCControllerDidConnectNotification object:nil];
1559 connectObserver = nil;
1560 }
1561
1562 if (disconnectObserver) {
1563 [center removeObserver:disconnectObserver name:GCControllerDidDisconnectNotification object:nil];
1564 disconnectObserver = nil;
1565 }
1566
1567#ifdef SDL_PLATFORM_TVOS
1568 SDL_RemoveHintCallback(SDL_HINT_APPLE_TV_REMOTE_ALLOW_ROTATION,
1569 SDL_AppleTVRemoteRotationHintChanged, NULL);
1570#endif // SDL_PLATFORM_TVOS
1571#endif // SDL_JOYSTICK_MFI
1572
1573 while (deviceList != NULL) {
1574 IOS_RemoveJoystickDevice(deviceList);
1575 }
1576 }
1577
1578 numjoysticks = 0;
1579}
1580
1581static bool IOS_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)
1582{
1583 SDL_JoystickDeviceItem *device = GetDeviceForIndex(device_index);
1584 if (device == NULL) {
1585 return false;
1586 }
1587
1588 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1589 int axis = 0;
1590 for (id key in device->axes) {
1591 if ([(NSString *)key isEqualToString:@"Left Thumbstick X Axis"] ||
1592 [(NSString *)key isEqualToString:@"Direction Pad X Axis"]) {
1593 out->leftx.kind = EMappingKind_Axis;
1594 out->leftx.target = axis;
1595 } else if ([(NSString *)key isEqualToString:@"Left Thumbstick Y Axis"] ||
1596 [(NSString *)key isEqualToString:@"Direction Pad Y Axis"]) {
1597 out->lefty.kind = EMappingKind_Axis;
1598 out->lefty.target = axis;
1599 out->lefty.axis_reversed = true;
1600 } else if ([(NSString *)key isEqualToString:@"Right Thumbstick X Axis"]) {
1601 out->rightx.kind = EMappingKind_Axis;
1602 out->rightx.target = axis;
1603 } else if ([(NSString *)key isEqualToString:@"Right Thumbstick Y Axis"]) {
1604 out->righty.kind = EMappingKind_Axis;
1605 out->righty.target = axis;
1606 out->righty.axis_reversed = true;
1607 } else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) {
1608 out->lefttrigger.kind = EMappingKind_Axis;
1609 out->lefttrigger.target = axis;
1610 out->lefttrigger.half_axis_positive = true;
1611 } else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) {
1612 out->righttrigger.kind = EMappingKind_Axis;
1613 out->righttrigger.target = axis;
1614 out->righttrigger.half_axis_positive = true;
1615 }
1616 ++axis;
1617 }
1618
1619 int button = 0;
1620 for (id key in device->buttons) {
1621 SDL_InputMapping *mapping = NULL;
1622
1623 if ([(NSString *)key isEqualToString:GCInputButtonA]) {
1624 if (device->is_siri_remote > 1) {
1625 // GCInputButtonA is triggered for any D-Pad press, ignore it in favor of "Button Center"
1626 } else if (device->has_nintendo_buttons) {
1627 mapping = &out->b;
1628 } else {
1629 mapping = &out->a;
1630 }
1631 } else if ([(NSString *)key isEqualToString:GCInputButtonB]) {
1632 if (device->has_nintendo_buttons) {
1633 mapping = &out->a;
1634 } else if (device->is_switch_joyconL || device->is_switch_joyconR) {
1635 mapping = &out->x;
1636 } else {
1637 mapping = &out->b;
1638 }
1639 } else if ([(NSString *)key isEqualToString:GCInputButtonX]) {
1640 if (device->has_nintendo_buttons) {
1641 mapping = &out->y;
1642 } else if (device->is_switch_joyconL || device->is_switch_joyconR) {
1643 mapping = &out->b;
1644 } else {
1645 mapping = &out->x;
1646 }
1647 } else if ([(NSString *)key isEqualToString:GCInputButtonY]) {
1648 if (device->has_nintendo_buttons) {
1649 mapping = &out->x;
1650 } else {
1651 mapping = &out->y;
1652 }
1653 } else if ([(NSString *)key isEqualToString:@"Direction Pad Left"]) {
1654 mapping = &out->dpleft;
1655 } else if ([(NSString *)key isEqualToString:@"Direction Pad Right"]) {
1656 mapping = &out->dpright;
1657 } else if ([(NSString *)key isEqualToString:@"Direction Pad Up"]) {
1658 mapping = &out->dpup;
1659 } else if ([(NSString *)key isEqualToString:@"Direction Pad Down"]) {
1660 mapping = &out->dpdown;
1661 } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Left"]) {
1662 mapping = &out->dpleft;
1663 } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Right"]) {
1664 mapping = &out->dpright;
1665 } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Up"]) {
1666 mapping = &out->dpup;
1667 } else if ([(NSString *)key isEqualToString:@"Cardinal Direction Pad Down"]) {
1668 mapping = &out->dpdown;
1669 } else if ([(NSString *)key isEqualToString:GCInputLeftShoulder]) {
1670 mapping = &out->leftshoulder;
1671 } else if ([(NSString *)key isEqualToString:GCInputRightShoulder]) {
1672 mapping = &out->rightshoulder;
1673 } else if ([(NSString *)key isEqualToString:GCInputLeftThumbstickButton]) {
1674 mapping = &out->leftstick;
1675 } else if ([(NSString *)key isEqualToString:GCInputRightThumbstickButton]) {
1676 mapping = &out->rightstick;
1677 } else if ([(NSString *)key isEqualToString:@"Button Home"]) {
1678 mapping = &out->guide;
1679 } else if ([(NSString *)key isEqualToString:GCInputButtonMenu]) {
1680 if (device->is_siri_remote) {
1681 mapping = &out->b;
1682 } else {
1683 mapping = &out->start;
1684 }
1685 } else if ([(NSString *)key isEqualToString:GCInputButtonOptions]) {
1686 mapping = &out->back;
1687 } else if ([(NSString *)key isEqualToString:@"Button Share"]) {
1688 mapping = &out->misc1;
1689 } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleOne]) {
1690 mapping = &out->right_paddle1;
1691 } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleTwo]) {
1692 mapping = &out->right_paddle2;
1693 } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleThree]) {
1694 mapping = &out->left_paddle1;
1695 } else if ([(NSString *)key isEqualToString:GCInputXboxPaddleFour]) {
1696 mapping = &out->left_paddle2;
1697 } else if ([(NSString *)key isEqualToString:GCInputLeftTrigger]) {
1698 mapping = &out->lefttrigger;
1699 } else if ([(NSString *)key isEqualToString:GCInputRightTrigger]) {
1700 mapping = &out->righttrigger;
1701 } else if ([(NSString *)key isEqualToString:GCInputDualShockTouchpadButton]) {
1702 mapping = &out->touchpad;
1703 } else if ([(NSString *)key isEqualToString:@"Button Center"]) {
1704 mapping = &out->a;
1705 }
1706 if (mapping && mapping->kind == EMappingKind_None) {
1707 mapping->kind = EMappingKind_Button;
1708 mapping->target = button;
1709 }
1710 ++button;
1711 }
1712
1713 return true;
1714 }
1715 return false;
1716}
1717
1718#if defined(SDL_JOYSTICK_MFI) && defined(SDL_PLATFORM_MACOS)
1719bool IOS_SupportedHIDDevice(IOHIDDeviceRef device)
1720{
1721 if (!SDL_GetHintBoolean(SDL_HINT_JOYSTICK_MFI, true)) {
1722 return false;
1723 }
1724
1725 if (@available(macOS 10.16, *)) {
1726 const int MAX_ATTEMPTS = 3;
1727 for (int attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
1728 if ([GCController supportsHIDDevice:device]) {
1729 return true;
1730 }
1731
1732 // The framework may not have seen the device yet
1733 SDL_Delay(10);
1734 }
1735 }
1736 return false;
1737}
1738#endif
1739
1740#ifdef SDL_JOYSTICK_MFI
1741/* NOLINTNEXTLINE(readability-non-const-parameter): getCString takes a non-const char* */
1742static void GetAppleSFSymbolsNameForElement(GCControllerElement *element, char *name)
1743{
1744 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1745 if (element) {
1746 [element.sfSymbolsName getCString:name maxLength:255 encoding:NSASCIIStringEncoding];
1747 }
1748 }
1749}
1750
1751static GCControllerDirectionPad *GetDirectionalPadForController(GCController *controller)
1752{
1753 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1754 return controller.physicalInputProfile.dpads[GCInputDirectionPad];
1755 }
1756
1757 if (controller.extendedGamepad) {
1758 return controller.extendedGamepad.dpad;
1759 }
1760
1761 if (controller.microGamepad) {
1762 return controller.microGamepad.dpad;
1763 }
1764
1765 return nil;
1766}
1767#endif // SDL_JOYSTICK_MFI
1768
1769const char *IOS_GetAppleSFSymbolsNameForButton(SDL_Gamepad *gamepad, SDL_GamepadButton button)
1770{
1771 char elementName[256];
1772 elementName[0] = '\0';
1773
1774#ifdef SDL_JOYSTICK_MFI
1775 if (gamepad && SDL_GetGamepadJoystick(gamepad)->driver == &SDL_IOS_JoystickDriver) {
1776 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1777 GCController *controller = SDL_GetGamepadJoystick(gamepad)->hwdata->controller;
1778 NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
1779 switch (button) {
1780 case SDL_GAMEPAD_BUTTON_SOUTH:
1781 GetAppleSFSymbolsNameForElement(elements[GCInputButtonA], elementName);
1782 break;
1783 case SDL_GAMEPAD_BUTTON_EAST:
1784 GetAppleSFSymbolsNameForElement(elements[GCInputButtonB], elementName);
1785 break;
1786 case SDL_GAMEPAD_BUTTON_WEST:
1787 GetAppleSFSymbolsNameForElement(elements[GCInputButtonX], elementName);
1788 break;
1789 case SDL_GAMEPAD_BUTTON_NORTH:
1790 GetAppleSFSymbolsNameForElement(elements[GCInputButtonY], elementName);
1791 break;
1792 case SDL_GAMEPAD_BUTTON_BACK:
1793 GetAppleSFSymbolsNameForElement(elements[GCInputButtonOptions], elementName);
1794 break;
1795 case SDL_GAMEPAD_BUTTON_GUIDE:
1796 GetAppleSFSymbolsNameForElement(elements[@"Button Home"], elementName);
1797 break;
1798 case SDL_GAMEPAD_BUTTON_START:
1799 GetAppleSFSymbolsNameForElement(elements[GCInputButtonMenu], elementName);
1800 break;
1801 case SDL_GAMEPAD_BUTTON_LEFT_STICK:
1802 GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstickButton], elementName);
1803 break;
1804 case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
1805 GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstickButton], elementName);
1806 break;
1807 case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
1808 GetAppleSFSymbolsNameForElement(elements[GCInputLeftShoulder], elementName);
1809 break;
1810 case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
1811 GetAppleSFSymbolsNameForElement(elements[GCInputRightShoulder], elementName);
1812 break;
1813 case SDL_GAMEPAD_BUTTON_DPAD_UP:
1814 {
1815 GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
1816 if (dpad) {
1817 GetAppleSFSymbolsNameForElement(dpad.up, elementName);
1818 if (SDL_strlen(elementName) == 0) {
1819 SDL_strlcpy(elementName, "dpad.up.fill", sizeof(elementName));
1820 }
1821 }
1822 break;
1823 }
1824 case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
1825 {
1826 GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
1827 if (dpad) {
1828 GetAppleSFSymbolsNameForElement(dpad.down, elementName);
1829 if (SDL_strlen(elementName) == 0) {
1830 SDL_strlcpy(elementName, "dpad.down.fill", sizeof(elementName));
1831 }
1832 }
1833 break;
1834 }
1835 case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
1836 {
1837 GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
1838 if (dpad) {
1839 GetAppleSFSymbolsNameForElement(dpad.left, elementName);
1840 if (SDL_strlen(elementName) == 0) {
1841 SDL_strlcpy(elementName, "dpad.left.fill", sizeof(elementName));
1842 }
1843 }
1844 break;
1845 }
1846 case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
1847 {
1848 GCControllerDirectionPad *dpad = GetDirectionalPadForController(controller);
1849 if (dpad) {
1850 GetAppleSFSymbolsNameForElement(dpad.right, elementName);
1851 if (SDL_strlen(elementName) == 0) {
1852 SDL_strlcpy(elementName, "dpad.right.fill", sizeof(elementName));
1853 }
1854 }
1855 break;
1856 }
1857 case SDL_GAMEPAD_BUTTON_MISC1:
1858 GetAppleSFSymbolsNameForElement(elements[GCInputDualShockTouchpadButton], elementName);
1859 break;
1860 case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1:
1861 GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleOne], elementName);
1862 break;
1863 case SDL_GAMEPAD_BUTTON_LEFT_PADDLE1:
1864 GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleThree], elementName);
1865 break;
1866 case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2:
1867 GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleTwo], elementName);
1868 break;
1869 case SDL_GAMEPAD_BUTTON_LEFT_PADDLE2:
1870 GetAppleSFSymbolsNameForElement(elements[GCInputXboxPaddleFour], elementName);
1871 break;
1872 case SDL_GAMEPAD_BUTTON_TOUCHPAD:
1873 GetAppleSFSymbolsNameForElement(elements[GCInputDualShockTouchpadButton], elementName);
1874 break;
1875 default:
1876 break;
1877 }
1878 }
1879 }
1880#endif // SDL_JOYSTICK_MFI
1881
1882 return *elementName ? SDL_GetPersistentString(elementName) : NULL;
1883}
1884
1885const char *IOS_GetAppleSFSymbolsNameForAxis(SDL_Gamepad *gamepad, SDL_GamepadAxis axis)
1886{
1887 char elementName[256];
1888 elementName[0] = '\0';
1889
1890#ifdef SDL_JOYSTICK_MFI
1891 if (gamepad && SDL_GetGamepadJoystick(gamepad)->driver == &SDL_IOS_JoystickDriver) {
1892 if (@available(macOS 10.16, iOS 14.0, tvOS 14.0, *)) {
1893 GCController *controller = SDL_GetGamepadJoystick(gamepad)->hwdata->controller;
1894 NSDictionary<NSString *, GCControllerElement *> *elements = controller.physicalInputProfile.elements;
1895 switch (axis) {
1896 case SDL_GAMEPAD_AXIS_LEFTX:
1897 GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstick], elementName);
1898 break;
1899 case SDL_GAMEPAD_AXIS_LEFTY:
1900 GetAppleSFSymbolsNameForElement(elements[GCInputLeftThumbstick], elementName);
1901 break;
1902 case SDL_GAMEPAD_AXIS_RIGHTX:
1903 GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstick], elementName);
1904 break;
1905 case SDL_GAMEPAD_AXIS_RIGHTY:
1906 GetAppleSFSymbolsNameForElement(elements[GCInputRightThumbstick], elementName);
1907 break;
1908 case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
1909 GetAppleSFSymbolsNameForElement(elements[GCInputLeftTrigger], elementName);
1910 break;
1911 case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
1912 GetAppleSFSymbolsNameForElement(elements[GCInputRightTrigger], elementName);
1913 break;
1914 default:
1915 break;
1916 }
1917 }
1918 }
1919#endif // SDL_JOYSTICK_MFI
1920
1921 return *elementName ? SDL_GetPersistentString(elementName) : NULL;
1922}
1923
1924SDL_JoystickDriver SDL_IOS_JoystickDriver = {
1925 IOS_JoystickInit,
1926 IOS_JoystickGetCount,
1927 IOS_JoystickDetect,
1928 IOS_JoystickIsDevicePresent,
1929 IOS_JoystickGetDeviceName,
1930 IOS_JoystickGetDevicePath,
1931 IOS_JoystickGetDeviceSteamVirtualGamepadSlot,
1932 IOS_JoystickGetDevicePlayerIndex,
1933 IOS_JoystickSetDevicePlayerIndex,
1934 IOS_JoystickGetDeviceGUID,
1935 IOS_JoystickGetDeviceInstanceID,
1936 IOS_JoystickOpen,
1937 IOS_JoystickRumble,
1938 IOS_JoystickRumbleTriggers,
1939 IOS_JoystickSetLED,
1940 IOS_JoystickSendEffect,
1941 IOS_JoystickSetSensorsEnabled,
1942 IOS_JoystickUpdate,
1943 IOS_JoystickClose,
1944 IOS_JoystickQuit,
1945 IOS_JoystickGetGamepadMapping
1946};