@geastack/apple 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +54 -0
  3. package/dist/index.d.ts +127 -0
  4. package/dist/index.js +3076 -0
  5. package/generated/AVFoundation.d.ts +112 -0
  6. package/generated/ActivityKit.d.ts +7 -0
  7. package/generated/AppKit.d.ts +268 -0
  8. package/generated/CoreGraphics.d.ts +18 -0
  9. package/generated/CoreLocation.d.ts +28 -0
  10. package/generated/CoreMedia.d.ts +8 -0
  11. package/generated/Dispatch.d.ts +2 -0
  12. package/generated/Foundation.d.ts +13 -0
  13. package/generated/MapKit.d.ts +28 -0
  14. package/generated/Metal.d.ts +91 -0
  15. package/generated/MetalKit.d.ts +22 -0
  16. package/generated/Photos.d.ts +5 -0
  17. package/generated/QuartzCore.d.ts +12 -0
  18. package/generated/SwiftData.d.ts +11 -0
  19. package/generated/UIKit.d.ts +187 -0
  20. package/package.json +123 -0
  21. package/runtime/AVFoundation.js +136 -0
  22. package/runtime/ActivityKit.js +18 -0
  23. package/runtime/AppKit.js +313 -0
  24. package/runtime/CoreGraphics.js +15 -0
  25. package/runtime/CoreLocation.js +30 -0
  26. package/runtime/CoreMedia.js +7 -0
  27. package/runtime/Dispatch.js +11 -0
  28. package/runtime/Foundation.js +21 -0
  29. package/runtime/MapKit.js +22 -0
  30. package/runtime/Metal.js +126 -0
  31. package/runtime/MetalKit.js +15 -0
  32. package/runtime/Photos.js +9 -0
  33. package/runtime/QuartzCore.js +9 -0
  34. package/runtime/SwiftData.js +29 -0
  35. package/runtime/UIKit.js +242 -0
  36. package/runtime/empty.js +1 -0
  37. package/targets/ios/Info.plist.in +25 -0
  38. package/targets/ios/README.md +46 -0
  39. package/targets/ios/build-ios.sh +395 -0
  40. package/targets/ios/detect-development-team.mjs +109 -0
  41. package/targets/ios/generate-resident-entry.mjs +67 -0
  42. package/targets/ios/generate-resident-registry.mjs +162 -0
  43. package/targets/ios/generate-xcode-project.mjs +520 -0
  44. package/targets/ios/main/canvas_view.h +7 -0
  45. package/targets/ios/main/canvas_view.mm +94 -0
  46. package/targets/ios/main/font_registry.h +17 -0
  47. package/targets/ios/main/font_registry.mm +129 -0
  48. package/targets/ios/main/image_bridge.h +13 -0
  49. package/targets/ios/main/image_bridge.mm +53 -0
  50. package/targets/ios/main/ios_apps.c +28 -0
  51. package/targets/ios/main/ios_camera.mm +446 -0
  52. package/targets/ios/main/ios_display.mm +344 -0
  53. package/targets/ios/main/ios_main.mm +811 -0
  54. package/targets/ios/main/ios_memory.cpp +55 -0
  55. package/targets/ios/main/ios_renderer.h +20 -0
  56. package/targets/ios/main/ios_renderer.mm +27 -0
  57. package/targets/ios/main/ios_root_background.cpp +20 -0
  58. package/targets/ios/main/ios_root_background.h +11 -0
  59. package/targets/ios/main/ios_sensors.mm +244 -0
  60. package/targets/ios/main/ios_timers.mm +53 -0
  61. package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
  62. package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
  63. package/targets/ios/main/renderer/native_button.mm +179 -0
  64. package/targets/ios/main/renderer/native_label.mm +161 -0
  65. package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
  66. package/targets/ios/main/renderer/view_reconciler.mm +274 -0
  67. package/targets/macos/Info.plist.in +26 -0
  68. package/targets/macos/README.md +131 -0
  69. package/targets/macos/build-child-lifecycle.sh +119 -0
  70. package/targets/macos/build-macos.sh +2156 -0
  71. package/targets/macos/check-module-graph-freshness.mjs +82 -0
  72. package/targets/macos/compiler-input-fingerprint.mjs +32 -0
  73. package/targets/macos/finalize-apple-native-output.mjs +77 -0
  74. package/targets/macos/generate-resident-entry.mjs +80 -0
  75. package/targets/macos/generate-resident-registry.mjs +141 -0
  76. package/targets/macos/heavy-build-lock.sh +176 -0
  77. package/targets/macos/main/canvas_view.h +9 -0
  78. package/targets/macos/main/canvas_view.mm +176 -0
  79. package/targets/macos/main/color_convert.h +18 -0
  80. package/targets/macos/main/color_convert.mm +39 -0
  81. package/targets/macos/main/font_registry.h +35 -0
  82. package/targets/macos/main/font_registry.mm +350 -0
  83. package/targets/macos/main/image_bridge.h +16 -0
  84. package/targets/macos/main/image_bridge.mm +52 -0
  85. package/targets/macos/main/macos_app_platform.mm +95 -0
  86. package/targets/macos/main/macos_apps.c +31 -0
  87. package/targets/macos/main/macos_device_control.mm +56 -0
  88. package/targets/macos/main/macos_display.mm +484 -0
  89. package/targets/macos/main/macos_host_device_control.mm +95 -0
  90. package/targets/macos/main/macos_main.mm +839 -0
  91. package/targets/macos/main/macos_memory.cpp +31 -0
  92. package/targets/macos/main/macos_native_shell.h +31 -0
  93. package/targets/macos/main/macos_native_shell.mm +396 -0
  94. package/targets/macos/main/macos_network.mm +253 -0
  95. package/targets/macos/main/macos_renderer.h +39 -0
  96. package/targets/macos/main/macos_renderer.mm +1969 -0
  97. package/targets/macos/main/macos_sensors.cpp +148 -0
  98. package/targets/macos/main/macos_smoke_app.cpp +65 -0
  99. package/targets/macos/main/macos_timers.mm +52 -0
  100. package/targets/macos/main/press_bridge.h +21 -0
  101. package/targets/macos/main/press_bridge.mm +72 -0
  102. package/targets/macos/main/thermalright_hid_display.h +14 -0
  103. package/targets/macos/main/thermalright_hid_display.mm +995 -0
  104. package/targets/macos/native-artifact-provenance.mjs +421 -0
  105. package/targets/macos/order-compile-edges.mjs +64 -0
  106. package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
  107. package/targets/macos/prune-generated-modules.mjs +147 -0
  108. package/targets/macos/sync-resources.mjs +153 -0
  109. package/targets/shared/analyze-audio-capture.mjs +131 -0
  110. package/targets/shared/apple_audio.mm +1194 -0
  111. package/targets/shared/apple_audio_selftest.mm +602 -0
  112. package/targets/shared/apple_audio_testing.h +34 -0
  113. package/targets/shared/build-audio-selftest.sh +50 -0
@@ -0,0 +1,811 @@
1
+ #import <UIKit/UIKit.h>
2
+ #import <QuartzCore/QuartzCore.h>
3
+
4
+ #include "app.h"
5
+ #include "apps.h"
6
+ #include "canvas.h"
7
+ #include "display.h"
8
+ #include "event.h"
9
+ #include "events.h"
10
+ #include "font_registry.h"
11
+ #include "image.h"
12
+ #include "ios_renderer.h"
13
+ #include "resident_apps.h"
14
+ #include "ios_root_background.h"
15
+ #include "pixel.h"
16
+ #include "ui/tree_internal.h"
17
+
18
+ #include <algorithm>
19
+ #include <cmath>
20
+ #include <cstddef>
21
+ #include <cstdint>
22
+ #include <cstdlib>
23
+ #include <cstring>
24
+
25
+ #if __has_include("gea/apple/native_bridge.h")
26
+ #include "gea/apple/native_bridge.h"
27
+ #define GEA_IOS_HAS_APPLE_NATIVE_BRIDGE 1
28
+ #endif
29
+
30
+ extern "C" int gea_embedded_now_ms(void);
31
+ extern "C" void gea_ios_display_set_viewport_size(int width, int height);
32
+ extern "C" const std::uint32_t *gea_ios_display_presented_pixels();
33
+ extern "C" void gea_ios_touch_set_state(int touching, int x, int y);
34
+
35
+ @class GeaAppDelegate;
36
+ static __weak GeaAppDelegate *gGeaAppDelegate = nil;
37
+
38
+ // Wires the iOS AVFoundation camera backend (ios_camera.mm) to the runtime's
39
+ // board-independent <camera> preview seam. Defined in @geastack/host's host/camera.cpp.
40
+ namespace gea::framework::camera {
41
+ void registerCameraSurface();
42
+ }
43
+
44
+ namespace {
45
+
46
+ UIColor *rgb565ToUIColor(gea::framework::graphics::pixel::native_t color)
47
+ {
48
+ // Style colours are native pixels (RGBA8888 on iOS) — unpack full 8-bit
49
+ // channels so native UIKit views render true colour, not 565-quantized.
50
+ int r, g, b, a;
51
+ gea::framework::graphics::pixel::unpackNative8(color, &r, &g, &b, &a);
52
+ return [UIColor colorWithRed:static_cast<CGFloat>(r) / 255.0
53
+ green:static_cast<CGFloat>(g) / 255.0
54
+ blue:static_cast<CGFloat>(b) / 255.0
55
+ alpha:1.0];
56
+ }
57
+
58
+ NSString *NSStringFromAttr(const char *value)
59
+ {
60
+ return [NSString stringWithUTF8String:value ? value : ""] ?: @"";
61
+ }
62
+
63
+ void queueInitialResidentAppFromProcessArguments()
64
+ {
65
+ if (!gea::framework::apps::ResidentApps::isEnabled()) return;
66
+ NSArray<NSString *> *arguments = NSProcessInfo.processInfo.arguments;
67
+ for (NSUInteger i = 0; i + 1 < arguments.count; ++i) {
68
+ if (![arguments[i] isEqualToString:@"--gea-initial-app"]) continue;
69
+ NSString *appId = arguments[i + 1];
70
+ if (appId.length > 0) gea::framework::apps::ResidentApps::requestLaunch(appId.UTF8String);
71
+ }
72
+ const char *envAppId = std::getenv("GEA_IOS_INITIAL_APP");
73
+ if (envAppId && envAppId[0]) gea::framework::apps::ResidentApps::requestLaunch(envAppId);
74
+ }
75
+
76
+ NSTextAlignment textAlignmentForStyle(int align)
77
+ {
78
+ switch (align) {
79
+ case 1: return NSTextAlignmentCenter;
80
+ case 2: return NSTextAlignmentRight;
81
+ default: return NSTextAlignmentLeft;
82
+ }
83
+ }
84
+
85
+ void addTextDecorationAttributes(NSMutableDictionary *attrs, int textDecoration)
86
+ {
87
+ if (!attrs) return;
88
+ NSNumber *style = @((NSInteger)NSUnderlineStyleSingle);
89
+ if (textDecoration == 1) {
90
+ attrs[NSUnderlineStyleAttributeName] = style;
91
+ } else if (textDecoration == 2) {
92
+ attrs[NSStrikethroughStyleAttributeName] = style;
93
+ }
94
+ }
95
+
96
+ NSMutableDictionary *textAttributes(UIFont *font, UIColor *color, int textDecoration)
97
+ {
98
+ NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
99
+ attrs[NSFontAttributeName] = font ?: [UIFont systemFontOfSize:16];
100
+ attrs[NSForegroundColorAttributeName] = color ?: UIColor.blackColor;
101
+ addTextDecorationAttributes(attrs, textDecoration);
102
+ return attrs;
103
+ }
104
+
105
+ bool attributedStringHasTextDecoration(NSAttributedString *value)
106
+ {
107
+ if (!value || value.length == 0) return false;
108
+ return [value attribute:NSUnderlineStyleAttributeName atIndex:0 effectiveRange:nil] != nil ||
109
+ [value attribute:NSStrikethroughStyleAttributeName atIndex:0 effectiveRange:nil] != nil;
110
+ }
111
+
112
+ } // namespace
113
+
114
+ @interface GeaNativeInputField : UITextField <UITextFieldDelegate>
115
+ @property(nonatomic, assign) int nodeId;
116
+ @property(nonatomic, assign) BOOL syncingFromTree;
117
+ @property(nonatomic, assign) UIEdgeInsets textInsets;
118
+ @end
119
+
120
+ @implementation GeaNativeInputField
121
+
122
+ - (instancetype)initWithFrame:(CGRect)frame
123
+ {
124
+ self = [super initWithFrame:frame];
125
+ if (self) {
126
+ _nodeId = -1;
127
+ _syncingFromTree = NO;
128
+ _textInsets = UIEdgeInsetsZero;
129
+ self.delegate = self;
130
+ self.borderStyle = UITextBorderStyleNone;
131
+ self.backgroundColor = UIColor.clearColor;
132
+ self.opaque = NO;
133
+ self.autocorrectionType = UITextAutocorrectionTypeNo;
134
+ self.autocapitalizationType = UITextAutocapitalizationTypeNone;
135
+ self.spellCheckingType = UITextSpellCheckingTypeNo;
136
+ self.returnKeyType = UIReturnKeyDone;
137
+ if (@available(iOS 11.0, *)) {
138
+ self.smartDashesType = UITextSmartDashesTypeNo;
139
+ self.smartQuotesType = UITextSmartQuotesTypeNo;
140
+ self.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
141
+ }
142
+ [self addTarget:self action:@selector(editingChanged:) forControlEvents:UIControlEventEditingChanged];
143
+ }
144
+ return self;
145
+ }
146
+
147
+ - (CGRect)textRectForBounds:(CGRect)bounds
148
+ {
149
+ return UIEdgeInsetsInsetRect(bounds, self.textInsets);
150
+ }
151
+
152
+ - (CGRect)editingRectForBounds:(CGRect)bounds
153
+ {
154
+ return UIEdgeInsetsInsetRect(bounds, self.textInsets);
155
+ }
156
+
157
+ - (CGRect)placeholderRectForBounds:(CGRect)bounds
158
+ {
159
+ return UIEdgeInsetsInsetRect(bounds, self.textInsets);
160
+ }
161
+
162
+ - (void)dispatchEventOfType:(gea::framework::events::PointerEventType)type withKeyCode:(int)keyCode
163
+ {
164
+ using gea::framework::events::PointerEvent;
165
+ if (_nodeId < 0) return;
166
+ auto &tree = gea::embedded::ui::Tree::instance();
167
+ if (_nodeId >= tree.nodeCount()) return;
168
+ PointerEvent event;
169
+ event.type = type;
170
+ event.targetId = _nodeId;
171
+ event.keyCode = keyCode;
172
+ tree.dispatchEvent(event);
173
+ }
174
+
175
+ - (void)editingChanged:(id)sender
176
+ {
177
+ (void)sender;
178
+ if (_syncingFromTree || _nodeId < 0) return;
179
+ auto &tree = gea::embedded::ui::Tree::instance();
180
+ if (_nodeId >= tree.nodeCount()) return;
181
+ const char *utf8 = self.text ? [self.text UTF8String] : "";
182
+ tree.setAttribute(_nodeId, "value", utf8 ? utf8 : "");
183
+ [self dispatchEventOfType:gea::framework::events::PointerEventType::Input withKeyCode:0];
184
+ }
185
+
186
+ - (void)textFieldDidBeginEditing:(UITextField *)textField
187
+ {
188
+ (void)textField;
189
+ if (_nodeId < 0) return;
190
+ gea::embedded::ui::Tree::instance().setActiveInput(_nodeId);
191
+ }
192
+
193
+ - (void)textFieldDidEndEditing:(UITextField *)textField
194
+ {
195
+ (void)textField;
196
+ auto &tree = gea::embedded::ui::Tree::instance();
197
+ if (tree.activeInputId() == _nodeId) tree.setActiveInput(-1);
198
+ }
199
+
200
+ - (BOOL)textFieldShouldReturn:(UITextField *)textField
201
+ {
202
+ (void)textField;
203
+ [self dispatchEventOfType:gea::framework::events::PointerEventType::KeyDown withKeyCode:13];
204
+ return YES;
205
+ }
206
+
207
+ - (void)deleteBackward
208
+ {
209
+ [self dispatchEventOfType:gea::framework::events::PointerEventType::KeyDown withKeyCode:8];
210
+ [super deleteBackward];
211
+ }
212
+
213
+ @end
214
+
215
+ @interface GeaDisplayView : UIView
216
+ @property(nonatomic, strong) NSMapTable<UITouch *, NSNumber *> *touchPointerIds;
217
+ - (void)syncNativeTextInputs;
218
+ @end
219
+
220
+ @implementation GeaDisplayView
221
+
222
+ - (instancetype)initWithFrame:(CGRect)frame
223
+ {
224
+ self = [super initWithFrame:frame];
225
+ if (self) {
226
+ self.opaque = YES;
227
+ self.backgroundColor = UIColor.blackColor;
228
+ self.contentMode = UIViewContentModeRedraw;
229
+ self.userInteractionEnabled = YES;
230
+ // Track multiple simultaneous fingers so one button can be held while
231
+ // another is tapped (e.g. move + jump). Keyed by touch identity; the
232
+ // value is the pointer index (0 = primary).
233
+ self.multipleTouchEnabled = YES;
234
+ self.touchPointerIds = [NSMapTable mapTableWithKeyOptions:NSMapTableWeakMemory | NSMapTableObjectPointerPersonality
235
+ valueOptions:NSMapTableStrongMemory];
236
+ }
237
+ return self;
238
+ }
239
+
240
+ - (CGFloat)nativeContentScale
241
+ {
242
+ CGFloat scale = self.contentScaleFactor;
243
+ UIScreen *screen = self.window.screen ?: UIScreen.mainScreen;
244
+ if (scale <= 0) scale = screen.nativeScale;
245
+ if (scale <= 0) scale = screen.scale;
246
+ return scale > 0 ? scale : 1;
247
+ }
248
+
249
+ - (CGPoint)nativePointForTouch:(UITouch *)touch
250
+ {
251
+ return [self nativePointForViewPoint:[touch locationInView:self]];
252
+ }
253
+
254
+ - (CGPoint)nativePointForViewPoint:(CGPoint)point
255
+ {
256
+ // Map view points into canvas/layout coordinates by the actual view->canvas
257
+ // ratio. The canvas is sized in logical points, so this is ~1; for a
258
+ // physical-pixel canvas it equals the content scale factor. Using the raw
259
+ // content scale factor unconditionally assumed a physical-pixel canvas.
260
+ auto *canvas = gea::platform::display::Display::canvas();
261
+ const CGRect bounds = self.bounds;
262
+ if (canvas && bounds.size.width > 0 && bounds.size.height > 0) {
263
+ point.x *= static_cast<CGFloat>(canvas->width()) / bounds.size.width;
264
+ point.y *= static_cast<CGFloat>(canvas->height()) / bounds.size.height;
265
+ }
266
+ return point;
267
+ }
268
+
269
+ - (int)clampedTouchX:(CGFloat)x
270
+ {
271
+ int ix = static_cast<int>(std::lround(x));
272
+ if (auto *canvas = gea::platform::display::Display::canvas()) {
273
+ ix = std::clamp(ix, 0, std::max(0, canvas->width() - 1));
274
+ }
275
+ return std::max(0, ix);
276
+ }
277
+
278
+ - (int)clampedTouchY:(CGFloat)y
279
+ {
280
+ int iy = static_cast<int>(std::lround(y));
281
+ if (auto *canvas = gea::platform::display::Display::canvas()) {
282
+ iy = std::clamp(iy, 0, std::max(0, canvas->height() - 1));
283
+ }
284
+ return std::max(0, iy);
285
+ }
286
+
287
+ - (CGRect)canvasDestinationRect
288
+ {
289
+ auto *canvas = gea::platform::display::Display::canvas();
290
+ if (!canvas || canvas->width() <= 0 || canvas->height() <= 0) return self.bounds;
291
+ const CGRect bounds = self.bounds;
292
+ const CGFloat scale = std::min(bounds.size.width / static_cast<CGFloat>(canvas->width()),
293
+ bounds.size.height / static_cast<CGFloat>(canvas->height()));
294
+ const CGSize size = CGSizeMake(static_cast<CGFloat>(canvas->width()) * scale,
295
+ static_cast<CGFloat>(canvas->height()) * scale);
296
+ return CGRectMake(bounds.origin.x + (bounds.size.width - size.width) * 0.5,
297
+ bounds.origin.y + (bounds.size.height - size.height) * 0.5,
298
+ size.width,
299
+ size.height);
300
+ }
301
+
302
+ - (void)syncNativeTextInputs
303
+ {
304
+ using namespace gea::embedded::ui;
305
+ static NSMutableDictionary<NSNumber *, GeaNativeInputField *> *fields = nil;
306
+ if (!fields) fields = [NSMutableDictionary new];
307
+
308
+ auto *canvas = gea::platform::display::Display::canvas();
309
+ if (!canvas || canvas->width() <= 0 || canvas->height() <= 0) {
310
+ for (GeaNativeInputField *field in [fields allValues]) {
311
+ [field resignFirstResponder];
312
+ [field removeFromSuperview];
313
+ }
314
+ [fields removeAllObjects];
315
+ return;
316
+ }
317
+
318
+ const CGRect dest = [self canvasDestinationRect];
319
+ const CGFloat canvasScale = dest.size.width / static_cast<CGFloat>(canvas->width());
320
+ auto &tree = Tree::instance();
321
+ const int activeInput = tree.activeInputId();
322
+ NSMutableSet<NSNumber *> *seen = [NSMutableSet set];
323
+
324
+ for (int nodeId = 0; nodeId < tree.nodeCount(); nodeId++) {
325
+ const Node &node = tree.node(nodeId);
326
+ const char *tagName = tree.tagName(nodeId);
327
+ if (node.type != NodeType::View || !tagName || std::strcmp(tagName, "input") != 0) continue;
328
+
329
+ NSNumber *key = @(nodeId);
330
+ [seen addObject:key];
331
+ GeaNativeInputField *field = fields[key];
332
+ if (!field) {
333
+ field = [[GeaNativeInputField alloc] initWithFrame:CGRectZero];
334
+ fields[key] = field;
335
+ [self addSubview:field];
336
+ } else if (field.superview != self) {
337
+ [field removeFromSuperview];
338
+ [self addSubview:field];
339
+ }
340
+
341
+ field.nodeId = nodeId;
342
+ const CGFloat x = dest.origin.x + static_cast<CGFloat>(node.layout.x) * canvasScale;
343
+ const CGFloat y = dest.origin.y + static_cast<CGFloat>(node.layout.y) * canvasScale;
344
+ const CGFloat w = static_cast<CGFloat>(node.layout.width) * canvasScale;
345
+ const CGFloat h = static_cast<CGFloat>(node.layout.height) * canvasScale;
346
+ field.frame = CGRectMake(x, y, w, h);
347
+ field.textInsets = UIEdgeInsetsMake(static_cast<CGFloat>(std::max<int>(0, node.style.padding[0])) * canvasScale,
348
+ static_cast<CGFloat>(std::max<int>(0, node.style.padding[3])) * canvasScale,
349
+ static_cast<CGFloat>(std::max<int>(0, node.style.padding[2])) * canvasScale,
350
+ static_cast<CGFloat>(std::max<int>(0, node.style.padding[1])) * canvasScale);
351
+ field.hidden = (node.style.display == 1 || node.style.opacity == 0 || w <= 0 || h <= 0);
352
+ field.userInteractionEnabled = !field.hidden;
353
+ field.alpha = static_cast<CGFloat>(node.style.opacity) / 255.0;
354
+ field.backgroundColor = node.style.has_bg ? rgb565ToUIColor(node.style.bg_color) : UIColor.clearColor;
355
+ field.layer.borderWidth = static_cast<CGFloat>(std::max<int>(0, node.style.border_width)) * canvasScale;
356
+ field.layer.borderColor = rgb565ToUIColor(node.style.border_color).CGColor;
357
+ const int tl = std::max<int>(0, node.style.border_radius[0]);
358
+ const int tr = std::max<int>(0, node.style.border_radius[1]);
359
+ const int br = std::max<int>(0, node.style.border_radius[2]);
360
+ const int bl = std::max<int>(0, node.style.border_radius[3]);
361
+ const CGFloat radius = (tl == tr && tr == br && br == bl)
362
+ ? static_cast<CGFloat>(tl)
363
+ : static_cast<CGFloat>(tl + tr + br + bl) / 4.0;
364
+ field.layer.cornerRadius = radius * canvasScale;
365
+ field.layer.masksToBounds = field.layer.cornerRadius > 0;
366
+ const CGFloat fontSize = std::max<CGFloat>(1.0, static_cast<CGFloat>(node.style.font_size > 0 ? node.style.font_size : 16) * canvasScale);
367
+ field.textColor = rgb565ToUIColor(node.style.text_color);
368
+ field.font = gea::ios::fontForId(node.style.font_id, fontSize);
369
+ field.textAlignment = textAlignmentForStyle(node.style.text_align);
370
+ NSMutableDictionary *textAttrs = textAttributes(field.font, field.textColor, node.style.text_decoration);
371
+ field.defaultTextAttributes = textAttrs;
372
+
373
+ const char *typeAttr = tree.getAttribute(nodeId, "type");
374
+ NSString *type = [NSStringFromAttr(typeAttr) lowercaseString];
375
+ field.secureTextEntry = [type isEqualToString:@"password"];
376
+ if ([type isEqualToString:@"email"]) field.keyboardType = UIKeyboardTypeEmailAddress;
377
+ else if ([type isEqualToString:@"number"]) field.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
378
+ else if ([type isEqualToString:@"tel"]) field.keyboardType = UIKeyboardTypePhonePad;
379
+ else if ([type isEqualToString:@"url"]) field.keyboardType = UIKeyboardTypeURL;
380
+ else field.keyboardType = UIKeyboardTypeDefault;
381
+
382
+ NSString *placeholder = NSStringFromAttr(tree.getAttribute(nodeId, "placeholder"));
383
+ field.placeholder = placeholder.length > 0 ? placeholder : nil;
384
+
385
+ NSString *desiredValue = NSStringFromAttr(tree.getAttribute(nodeId, "value"));
386
+ const BOOL textDiffers = ![(field.text ?: @"") isEqualToString:desiredValue];
387
+ const BOOL usesDecoratedText = node.style.text_decoration != 0;
388
+ if (textDiffers) {
389
+ field.syncingFromTree = YES;
390
+ if (usesDecoratedText) {
391
+ field.attributedText = [[NSAttributedString alloc] initWithString:desiredValue attributes:textAttrs];
392
+ } else {
393
+ field.text = desiredValue;
394
+ }
395
+ field.syncingFromTree = NO;
396
+ } else if (!field.isFirstResponder) {
397
+ if (usesDecoratedText) {
398
+ NSAttributedString *decoratedValue = [[NSAttributedString alloc] initWithString:desiredValue attributes:textAttrs];
399
+ if (![field.attributedText isEqualToAttributedString:decoratedValue]) {
400
+ field.syncingFromTree = YES;
401
+ field.attributedText = decoratedValue;
402
+ field.syncingFromTree = NO;
403
+ }
404
+ } else if (attributedStringHasTextDecoration(field.attributedText)) {
405
+ field.syncingFromTree = YES;
406
+ field.text = desiredValue;
407
+ field.syncingFromTree = NO;
408
+ }
409
+ }
410
+
411
+ if (activeInput == nodeId && !field.hidden) {
412
+ if (!field.isFirstResponder) [field becomeFirstResponder];
413
+ } else if (field.isFirstResponder) {
414
+ [field resignFirstResponder];
415
+ }
416
+ if (!field.hidden) [self bringSubviewToFront:field];
417
+ }
418
+
419
+ for (NSNumber *key in [fields allKeys]) {
420
+ if ([seen containsObject:key]) continue;
421
+ GeaNativeInputField *field = fields[key];
422
+ [field resignFirstResponder];
423
+ [field removeFromSuperview];
424
+ [fields removeObjectForKey:key];
425
+ }
426
+ }
427
+
428
+ - (int)pointerIdForTouch:(UITouch *)touch assignIfAbsent:(BOOL)assign
429
+ {
430
+ NSNumber *existing = [self.touchPointerIds objectForKey:touch];
431
+ if (existing) return existing.intValue;
432
+ if (!assign) return -1;
433
+ // Reuse the lowest free pointer index so ids stay small and stable.
434
+ unsigned int used = 0;
435
+ for (NSNumber *value in [[self.touchPointerIds objectEnumerator] allObjects]) {
436
+ const int v = value.intValue;
437
+ if (v >= 0 && v < 31) used |= (1u << v);
438
+ }
439
+ int pointerId = 0;
440
+ while (pointerId < 31 && (used & (1u << pointerId))) pointerId++;
441
+ [self.touchPointerIds setObject:@(pointerId) forKey:touch];
442
+ return pointerId;
443
+ }
444
+
445
+ - (void)dispatchTouchPhase:(gea::framework::events::TouchPhase)phase
446
+ touching:(bool)touching
447
+ nativeX:(int)x
448
+ nativeY:(int)y
449
+ pointerId:(int)pointerId
450
+ {
451
+ // Only the primary pointer (0) drives the single-point hardware touch state
452
+ // that polling apps read; every finger dispatches DOM-style touch events.
453
+ if (pointerId == 0) gea_ios_touch_set_state(touching ? 1 : 0, x, y);
454
+ gea::framework::events::Event event{};
455
+ event.type = gea::framework::events::EventType::Touch;
456
+ event.touchPhase = phase;
457
+ event.touching = touching;
458
+ event.x = x;
459
+ event.y = y;
460
+ event.pointerId = pointerId;
461
+ gea::framework::events::TouchRuntime::dispatchEvent(event);
462
+ }
463
+
464
+ - (void)dispatchTouch:(UITouch *)touch
465
+ phase:(gea::framework::events::TouchPhase)phase
466
+ touching:(bool)touching
467
+ {
468
+ if (!touch) return;
469
+ const BOOL isDown = phase == gea::framework::events::TouchPhase::Down;
470
+ const int pointerId = [self pointerIdForTouch:touch assignIfAbsent:isDown];
471
+ if (pointerId < 0) return;
472
+ const CGPoint point = [self nativePointForTouch:touch];
473
+ const int x = [self clampedTouchX:point.x];
474
+ const int y = [self clampedTouchY:point.y];
475
+ [self dispatchTouchPhase:phase touching:touching nativeX:x nativeY:y pointerId:pointerId];
476
+ if (phase == gea::framework::events::TouchPhase::Up) {
477
+ [self.touchPointerIds removeObjectForKey:touch];
478
+ }
479
+ }
480
+
481
+ - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
482
+ {
483
+ (void)event;
484
+ for (UITouch *touch in touches) {
485
+ [self dispatchTouch:touch phase:gea::framework::events::TouchPhase::Down touching:true];
486
+ }
487
+ }
488
+
489
+ - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
490
+ {
491
+ (void)event;
492
+ for (UITouch *touch in touches) {
493
+ [self dispatchTouch:touch phase:gea::framework::events::TouchPhase::Move touching:true];
494
+ }
495
+ }
496
+
497
+ - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
498
+ {
499
+ (void)event;
500
+ for (UITouch *touch in touches) {
501
+ [self dispatchTouch:touch phase:gea::framework::events::TouchPhase::Up touching:false];
502
+ }
503
+ }
504
+
505
+ - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
506
+ {
507
+ (void)event;
508
+ for (UITouch *touch in touches) {
509
+ [self dispatchTouch:touch phase:gea::framework::events::TouchPhase::Up touching:false];
510
+ }
511
+ }
512
+
513
+ - (void)drawRect:(CGRect)dirtyRect
514
+ {
515
+ (void)dirtyRect;
516
+ if (gea::ios::IosRenderer::instance().hasNativeTree()) return;
517
+ auto *canvas = gea::platform::display::Display::canvas();
518
+ if (!canvas) return;
519
+ const std::uint32_t *src = gea_ios_display_presented_pixels();
520
+ const int w = canvas->width();
521
+ const int h = canvas->height();
522
+ if (!src || w <= 0 || h <= 0) return;
523
+
524
+ // The framebuffer is already RGBA8888 (bytes R,G,B,A per pixel) — copy it
525
+ // straight into the CGImage scratch; no RGB565 expansion needed.
526
+ static std::uint8_t *scratch = nullptr;
527
+ static std::size_t scratchBytes = 0;
528
+ const std::size_t bytes = static_cast<std::size_t>(w) * h * 4;
529
+ if (scratchBytes < bytes) {
530
+ void *grown = std::realloc(scratch, bytes);
531
+ if (!grown) return;
532
+ scratch = static_cast<std::uint8_t *>(grown);
533
+ scratchBytes = bytes;
534
+ }
535
+ std::memcpy(scratch, src, bytes);
536
+
537
+ CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
538
+ CGDataProviderRef provider = CGDataProviderCreateWithData(nullptr, scratch, bytes, nullptr);
539
+ const uint32_t bitmapInfo = static_cast<uint32_t>(kCGImageAlphaNoneSkipLast) |
540
+ static_cast<uint32_t>(kCGBitmapByteOrder32Big);
541
+ CGImageRef image = CGImageCreate(static_cast<std::size_t>(w), static_cast<std::size_t>(h),
542
+ 8, 32, static_cast<std::size_t>(w) * 4,
543
+ cs, bitmapInfo, provider, nullptr, false,
544
+ kCGRenderingIntentDefault);
545
+
546
+ const CGRect bounds = self.bounds;
547
+ const CGFloat scale = std::min(bounds.size.width / static_cast<CGFloat>(w),
548
+ bounds.size.height / static_cast<CGFloat>(h));
549
+ const CGSize size = CGSizeMake(static_cast<CGFloat>(w) * scale,
550
+ static_cast<CGFloat>(h) * scale);
551
+ const CGRect dest = CGRectMake(bounds.origin.x + (bounds.size.width - size.width) * 0.5,
552
+ bounds.origin.y + (bounds.size.height - size.height) * 0.5,
553
+ size.width,
554
+ size.height);
555
+ UIImage *renderedImage = [UIImage imageWithCGImage:image];
556
+ [renderedImage drawInRect:dest];
557
+
558
+ CGImageRelease(image);
559
+ CGDataProviderRelease(provider);
560
+ CGColorSpaceRelease(cs);
561
+ }
562
+
563
+ @end
564
+
565
+ @interface GeaAppDelegate : UIResponder <UIApplicationDelegate>
566
+ @property(nonatomic, strong) UIWindow *window;
567
+ @property(nonatomic, strong) UIView *rootView;
568
+ @property(nonatomic, strong) GeaDisplayView *displayView;
569
+ @property(nonatomic, strong) UIView *nativeRootView;
570
+ @property(nonatomic, strong) UIButton *launcherButton;
571
+ @property(nonatomic, strong) CADisplayLink *displayLink;
572
+ - (void)installNativeRootView:(UIView *)view;
573
+ @end
574
+
575
+ @implementation GeaAppDelegate
576
+
577
+ - (BOOL)shouldShowLauncherButton
578
+ {
579
+ if (!gea::framework::apps::ResidentApps::isEnabled()) return NO;
580
+ const char *activeId = gea::framework::apps::ResidentApps::activeId();
581
+ return activeId && std::strcmp(activeId, gea::framework::apps::AppManager::launcherAppId()) != 0;
582
+ }
583
+
584
+ - (void)syncLauncherButton
585
+ {
586
+ if (!self.launcherButton || !self.rootView) return;
587
+ const BOOL show = [self shouldShowLauncherButton];
588
+ UIEdgeInsets safeInsets = self.rootView.safeAreaInsets;
589
+ const CGRect bounds = self.rootView.bounds;
590
+ const CGFloat size = 52.0;
591
+ const CGFloat bottomMargin = 12.0;
592
+ const CGFloat x = (bounds.size.width - size) * 0.5;
593
+ const CGFloat y = bounds.size.height - safeInsets.bottom - bottomMargin - size;
594
+ self.launcherButton.frame = CGRectMake(std::max<CGFloat>(safeInsets.left, x),
595
+ std::max<CGFloat>(safeInsets.top, y),
596
+ size,
597
+ size);
598
+ self.launcherButton.layer.cornerRadius = size * 0.5;
599
+ self.launcherButton.hidden = !show;
600
+ self.launcherButton.userInteractionEnabled = show;
601
+ if (show) [self.rootView bringSubviewToFront:self.launcherButton];
602
+ }
603
+
604
+ - (void)returnToLauncher:(id)sender
605
+ {
606
+ (void)sender;
607
+ if (gea::framework::apps::AppManager::returnRunningAppToLauncher("iOS launcher button")) {
608
+ self.launcherButton.hidden = YES;
609
+ self.launcherButton.userInteractionEnabled = NO;
610
+ }
611
+ }
612
+
613
+ - (void)layoutDisplayViewInSafeArea
614
+ {
615
+ if (!self.rootView || !self.displayView) return;
616
+ CGRect frame = self.rootView.safeAreaLayoutGuide.layoutFrame;
617
+ if (frame.size.width <= 0 || frame.size.height <= 0) {
618
+ frame = UIEdgeInsetsInsetRect(self.rootView.bounds, self.rootView.safeAreaInsets);
619
+ }
620
+ if (frame.size.width <= 0 || frame.size.height <= 0) frame = self.rootView.bounds;
621
+ self.displayView.frame = frame;
622
+ if (self.nativeRootView) self.nativeRootView.frame = self.rootView.bounds;
623
+ [self syncLauncherButton];
624
+ }
625
+
626
+ - (void)installNativeRootView:(UIView *)view
627
+ {
628
+ if (!view || !self.rootView) return;
629
+ if (self.nativeRootView && self.nativeRootView.superview == self.rootView) {
630
+ [self.nativeRootView removeFromSuperview];
631
+ }
632
+ self.nativeRootView = view;
633
+ view.frame = self.rootView.bounds;
634
+ view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
635
+ view.hidden = NO;
636
+ view.userInteractionEnabled = YES;
637
+ self.displayView.hidden = YES;
638
+ self.displayView.userInteractionEnabled = NO;
639
+ if (self.launcherButton.superview == self.rootView) {
640
+ [self.rootView insertSubview:view belowSubview:self.launcherButton];
641
+ } else {
642
+ [self.rootView addSubview:view];
643
+ }
644
+ [view setNeedsLayout];
645
+ [self syncLauncherButton];
646
+ }
647
+
648
+ - (CGSize)viewportSize
649
+ {
650
+ CGSize size = self.displayView.bounds.size;
651
+ if (size.width <= 0 || size.height <= 0) size = self.rootView.safeAreaLayoutGuide.layoutFrame.size;
652
+ if (size.width <= 0 || size.height <= 0) size = self.rootView.bounds.size;
653
+ if (size.width <= 0 || size.height <= 0) size = self.window.bounds.size;
654
+ if (size.width <= 0 || size.height <= 0) size = UIScreen.mainScreen.bounds.size;
655
+ return size;
656
+ }
657
+
658
+ - (CGFloat)viewportScale
659
+ {
660
+ UIScreen *screen = self.displayView.window.screen ?: self.window.screen ?: UIScreen.mainScreen;
661
+ CGFloat scale = screen.nativeScale;
662
+ if (scale <= 0) scale = screen.scale;
663
+ if (scale <= 0) scale = self.displayView.contentScaleFactor;
664
+ return scale > 0 ? scale : 1;
665
+ }
666
+
667
+ - (void)syncAppBackgroundColor
668
+ {
669
+ gea::framework::graphics::pixel::native_t rootColor = 0;
670
+ UIColor *color = gea::ios::mountedRootBackgroundColor(&rootColor) ? rgb565ToUIColor(rootColor) : UIColor.blackColor;
671
+ self.window.backgroundColor = color;
672
+ self.rootView.backgroundColor = color;
673
+ self.displayView.backgroundColor = color;
674
+ }
675
+
676
+ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
677
+ {
678
+ (void)application;
679
+ (void)launchOptions;
680
+ self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
681
+ UIViewController *controller = [[UIViewController alloc] init];
682
+ self.rootView = [[UIView alloc] initWithFrame:self.window.bounds];
683
+ self.rootView.opaque = YES;
684
+ self.rootView.backgroundColor = UIColor.blackColor;
685
+ self.rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
686
+ self.displayView = [[GeaDisplayView alloc] initWithFrame:CGRectZero];
687
+ self.displayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
688
+ [self.rootView addSubview:self.displayView];
689
+ self.launcherButton = [UIButton buttonWithType:UIButtonTypeSystem];
690
+ self.launcherButton.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.62];
691
+ self.launcherButton.tintColor = UIColor.whiteColor;
692
+ self.launcherButton.layer.borderWidth = 1.0;
693
+ self.launcherButton.layer.borderColor = [UIColor colorWithWhite:1.0 alpha:0.22].CGColor;
694
+ self.launcherButton.clipsToBounds = YES;
695
+ self.launcherButton.hidden = YES;
696
+ self.launcherButton.accessibilityLabel = @"App Launcher";
697
+ if (@available(iOS 13.0, *)) {
698
+ UIImage *image = [UIImage systemImageNamed:@"square.grid.2x2"];
699
+ [self.launcherButton setImage:image forState:UIControlStateNormal];
700
+ } else {
701
+ [self.launcherButton setTitle:@"Apps" forState:UIControlStateNormal];
702
+ self.launcherButton.titleLabel.font = [UIFont boldSystemFontOfSize:13.0];
703
+ }
704
+ [self.launcherButton addTarget:self action:@selector(returnToLauncher:) forControlEvents:UIControlEventTouchUpInside];
705
+ [self.rootView addSubview:self.launcherButton];
706
+ controller.view = self.rootView;
707
+ self.window.rootViewController = controller;
708
+ [self.window makeKeyAndVisible];
709
+ [self.rootView layoutIfNeeded];
710
+ [self layoutDisplayViewInSafeArea];
711
+ gGeaAppDelegate = self;
712
+
713
+ CGSize viewport = [self viewportSize];
714
+ const CGFloat viewportScale = [self viewportScale];
715
+ self.displayView.contentScaleFactor = viewportScale;
716
+ // Viewport/layout space is LOGICAL CSS px (points), NOT physical pixels.
717
+ // Multiplying by viewportScale here made window.innerWidth/innerHeight report
718
+ // physical pixels, so fixed-px app layouts (e.g. Sky Hop's 36px tiles)
719
+ // rendered ~3x too dense on retina. UIKit renders the native view tree
720
+ // crisply at the screen's content scale factor (set above) independently, so
721
+ // gea's internal CSS device-pixel-ratio stays 1 — otherwise `px` lengths get
722
+ // an extra x scale (cssPixelLength) that unitless lengths don't, blowing up
723
+ // fonts (16px/36px) relative to the unitless tile layout.
724
+ const int viewportWidth = std::max(1, static_cast<int>(std::ceil(viewport.width)));
725
+ const int viewportHeight = std::max(1, static_cast<int>(std::ceil(viewport.height)));
726
+ const int devicePixelRatio = 1;
727
+ gea_ios_display_set_viewport_size(viewportWidth, viewportHeight);
728
+ gea::platform::display::Display::init();
729
+ gea::framework::camera::registerCameraSurface();
730
+ // On iOS the build sets GEA_EMBEDDED_PIXEL_FORMAT=GEA_PIXEL_RGBA8888, so the
731
+ // ImageStore decodes straight to full-colour RGBA8888 native pixels (no 565
732
+ // quantization, no separate retained buffer) for native UIImageView.
733
+ gea::framework::app::Application::init(viewportWidth, viewportHeight, devicePixelRatio);
734
+ queueInitialResidentAppFromProcessArguments();
735
+ [self syncAppBackgroundColor];
736
+ [self tick:nil];
737
+
738
+ self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
739
+ if (@available(iOS 15.0, *)) {
740
+ self.displayLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 60, 60);
741
+ } else {
742
+ self.displayLink.preferredFramesPerSecond = 60;
743
+ }
744
+ [self.displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
745
+ return YES;
746
+ }
747
+
748
+ - (void)tick:(CADisplayLink *)link
749
+ {
750
+ (void)link;
751
+ {
752
+ char pendingId[64] = "";
753
+ if (gea::framework::apps::ResidentApps::consumeLaunch(pendingId, sizeof(pendingId)) &&
754
+ gea::framework::apps::ResidentApps::select(pendingId)) {
755
+ CGSize viewport = [self viewportSize];
756
+ const CGFloat viewportScale = [self viewportScale];
757
+ self.displayView.contentScaleFactor = viewportScale;
758
+ const int viewportWidth = std::max(1, static_cast<int>(std::ceil(viewport.width)));
759
+ const int viewportHeight = std::max(1, static_cast<int>(std::ceil(viewport.height)));
760
+ // gea's internal CSS device-pixel-ratio stays 1 (see init above): the
761
+ // layout is logical points; UIKit handles retina via contentScaleFactor.
762
+ const int devicePixelRatio = 1;
763
+ gea::ios::IosRenderer::instance().teardown();
764
+ gea_ios_display_set_viewport_size(viewportWidth, viewportHeight);
765
+ gea::platform::display::Display::init();
766
+ gea::framework::app::Application::init(viewportWidth, viewportHeight, devicePixelRatio);
767
+ }
768
+ }
769
+ gea::framework::app::Application::frame(gea_embedded_now_ms());
770
+ [self syncAppBackgroundColor];
771
+ [self syncLauncherButton];
772
+ auto &tree = gea::embedded::ui::Tree::instance();
773
+ const int root = tree.mountedRoot();
774
+ if (!self.nativeRootView && root >= 0) {
775
+ gea::ios::IosRenderer::instance().sync(self.displayView, root);
776
+ [self.displayView syncNativeTextInputs];
777
+ [self.displayView setNeedsDisplay];
778
+ }
779
+ }
780
+
781
+ @end
782
+
783
+ #if GEA_IOS_HAS_APPLE_NATIVE_BRIDGE
784
+ void gea::apple::UIKit::installRootView(gea::apple::UIKit::UIView view)
785
+ {
786
+ ::UIView *nativeView = (__bridge ::UIView *)gea::apple::objc::object(view.handle);
787
+ if (!nativeView) return;
788
+ if ([NSThread isMainThread]) {
789
+ [gGeaAppDelegate installNativeRootView:nativeView];
790
+ return;
791
+ }
792
+ dispatch_async(dispatch_get_main_queue(), ^{
793
+ [gGeaAppDelegate installNativeRootView:nativeView];
794
+ });
795
+ }
796
+ #endif
797
+
798
+ // The on-screen UIView hosting the gea framebuffer. The camera backend
799
+ // (ios_camera.mm) attaches its AVCaptureVideoPreviewLayer to this view's layer
800
+ // and positions it to a <camera> node's computed rect (NativeOverlay preview).
801
+ extern "C" UIView *gea_ios_camera_host_view(void)
802
+ {
803
+ return gGeaAppDelegate.displayView;
804
+ }
805
+
806
+ int main(int argc, char *argv[])
807
+ {
808
+ @autoreleasepool {
809
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([GeaAppDelegate class]));
810
+ }
811
+ }