@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,274 @@
1
+ #import <QuartzCore/QuartzCore.h>
2
+
3
+ #include "ios_renderer_internal.h"
4
+ #include "canvas_view.h"
5
+ #include "image_bridge.h"
6
+
7
+ #include "ui/tree_internal.h"
8
+
9
+ #import <objc/runtime.h>
10
+
11
+ #include <algorithm>
12
+ #include <cmath>
13
+ #include <cstring>
14
+
15
+ namespace gea::ios::renderer {
16
+ namespace {
17
+
18
+ bool nearlyEqual(CGFloat a, CGFloat b)
19
+ {
20
+ return std::fabs(a - b) <= 0.5;
21
+ }
22
+
23
+ bool rectNearlyEqual(CGRect a, CGRect b)
24
+ {
25
+ return nearlyEqual(a.origin.x, b.origin.x) &&
26
+ nearlyEqual(a.origin.y, b.origin.y) &&
27
+ nearlyEqual(a.size.width, b.size.width) &&
28
+ nearlyEqual(a.size.height, b.size.height);
29
+ }
30
+
31
+ bool viewMatchesNode(UIView *view, const gea::embedded::ui::Node &node, const char *tagName)
32
+ {
33
+ using gea::embedded::ui::NodeType;
34
+ if (node.type == NodeType::View && tagName && std::strcmp(tagName, "input") == 0) return false;
35
+ if (isScrollableNode(node)) return [view isKindOfClass:[GeaNativeScrollContainer class]];
36
+ switch (node.type) {
37
+ case NodeType::Text: return [view isKindOfClass:[GeaNativeLabel class]];
38
+ case NodeType::Button: return [view isKindOfClass:[GeaNativeButton class]];
39
+ case NodeType::Image: return [view isKindOfClass:[UIImageView class]];
40
+ case NodeType::Canvas: return [view isKindOfClass:[GeaCanvasView class]];
41
+ default: return [view isKindOfClass:[UIView class]] &&
42
+ ![view isKindOfClass:[UILabel class]] &&
43
+ ![view isKindOfClass:[UIControl class]] &&
44
+ ![view isKindOfClass:[UIImageView class]] &&
45
+ ![view isKindOfClass:[UIScrollView class]] &&
46
+ ![view isKindOfClass:[GeaCanvasView class]];
47
+ }
48
+ }
49
+
50
+ UIView *makeViewForType(gea::embedded::ui::NodeType type, const char *tagName,
51
+ const gea::embedded::ui::Node &node)
52
+ {
53
+ using gea::embedded::ui::NodeType;
54
+ if (type == NodeType::View && tagName && std::strcmp(tagName, "input") == 0) return nil;
55
+ if (isScrollableNode(node)) return [[GeaNativeScrollContainer alloc] initWithFrame:CGRectZero];
56
+ switch (type) {
57
+ case NodeType::Text: {
58
+ GeaNativeLabel *label = [[GeaNativeLabel alloc] initWithFrame:CGRectZero];
59
+ label.numberOfLines = 0;
60
+ label.lineBreakMode = NSLineBreakByWordWrapping;
61
+ label.userInteractionEnabled = NO;
62
+ label.backgroundColor = UIColor.clearColor;
63
+ return label;
64
+ }
65
+ case NodeType::Image: {
66
+ UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
67
+ imageView.userInteractionEnabled = NO;
68
+ return imageView;
69
+ }
70
+ case NodeType::Canvas: return [[GeaCanvasView alloc] initWithFrame:CGRectZero];
71
+ case NodeType::Button: {
72
+ GeaNativeButton *button = [GeaNativeButton buttonWithType:UIButtonTypeCustom];
73
+ button.nodeId = -1;
74
+ button.backgroundColor = UIColor.clearColor;
75
+ button.opaque = NO;
76
+ [button setTitle:nil forState:UIControlStateNormal];
77
+ [button addTarget:button action:@selector(handleTouchDown:) forControlEvents:UIControlEventTouchDown];
78
+ [button addTarget:button action:@selector(handleTouchDrag:) forControlEvents:UIControlEventTouchDragInside | UIControlEventTouchDragOutside];
79
+ [button addTarget:button action:@selector(handleTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
80
+ [button addTarget:button action:@selector(handleTouchUpOutside:) forControlEvents:UIControlEventTouchUpOutside];
81
+ [button addTarget:button action:@selector(handleTouchCancel:) forControlEvents:UIControlEventTouchCancel];
82
+ return button;
83
+ }
84
+ case NodeType::View:
85
+ case NodeType::VirtualList:
86
+ default: {
87
+ UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
88
+ view.userInteractionEnabled = NO;
89
+ return view;
90
+ }
91
+ }
92
+ }
93
+
94
+ void applyViewStyle(UIView *view, const gea::embedded::ui::Node &node,
95
+ int parentAbsX, int parentAbsY, CGFloat scale)
96
+ {
97
+ const CGFloat x = static_cast<CGFloat>(node.layout.x - parentAbsX) * scale;
98
+ const CGFloat y = static_cast<CGFloat>(node.layout.y - parentAbsY) * scale;
99
+ const CGFloat w = static_cast<CGFloat>(node.layout.width) * scale;
100
+ const CGFloat h = static_cast<CGFloat>(node.layout.height) * scale;
101
+
102
+ // UIKit's `frame` is undefined while a non-identity transform is applied, and a
103
+ // transform always rotates about the layer's anchor point (center) rather than
104
+ // the CSS transform-origin. Re-feeding `frame` every animation frame under a
105
+ // live rotation corrupts the bounds, ballooning rotated nodes (e.g. clock
106
+ // hands). For rotated nodes drive geometry through bounds + center + anchorPoint
107
+ // so the size stays correct and the rotation pivots about transform-origin;
108
+ // otherwise keep the plain frame path that scroll containers rely on.
109
+ if ((gea::embedded::ui::rstyle(node.style).transform_rotate % 3600) != 0) {
110
+ const CGFloat originX = static_cast<CGFloat>(gea::embedded::ui::rstyle(node.style).transform_origin_x) / 1000.0;
111
+ const CGFloat originY = static_cast<CGFloat>(gea::embedded::ui::rstyle(node.style).transform_origin_y) / 1000.0;
112
+ const CGPoint anchor = CGPointMake(originX, originY);
113
+ if (!CGPointEqualToPoint(view.layer.anchorPoint, anchor)) view.layer.anchorPoint = anchor;
114
+ if (!nearlyEqual(view.bounds.size.width, w) || !nearlyEqual(view.bounds.size.height, h)) {
115
+ CGRect bounds = view.bounds;
116
+ bounds.size = CGSizeMake(w, h);
117
+ view.bounds = bounds;
118
+ }
119
+ const CGPoint center = CGPointMake(x + originX * w, y + originY * h);
120
+ if (!nearlyEqual(view.center.x, center.x) || !nearlyEqual(view.center.y, center.y)) {
121
+ view.center = center;
122
+ }
123
+ const CGFloat degrees = static_cast<CGFloat>(gea::embedded::ui::rstyle(node.style).transform_rotate) / 10.0;
124
+ view.transform = CGAffineTransformMakeRotation(degrees * static_cast<CGFloat>(M_PI / 180.0));
125
+ } else {
126
+ if (!CGAffineTransformIsIdentity(view.transform)) view.transform = CGAffineTransformIdentity;
127
+ if (!CGPointEqualToPoint(view.layer.anchorPoint, CGPointMake(0.5, 0.5))) {
128
+ view.layer.anchorPoint = CGPointMake(0.5, 0.5);
129
+ }
130
+ const CGRect nextFrame = CGRectMake(x, y, w, h);
131
+ if (!rectNearlyEqual(view.frame, nextFrame)) view.frame = nextFrame;
132
+ }
133
+ view.hidden = node.style.display == 1 || node.style.opacity == 0 || w <= 0 || h <= 0;
134
+ view.alpha = static_cast<CGFloat>(node.style.opacity) / 255.0;
135
+
136
+ view.backgroundColor = node.style.has_bg ? rgb565ToUIColor(node.style.bg_color) : UIColor.clearColor;
137
+ view.layer.borderWidth = static_cast<CGFloat>(std::max<int>(0, node.style.border_width)) * scale;
138
+ view.layer.borderColor = rgb565ToUIColor(node.style.border_color).CGColor;
139
+ const int tl = std::max<int>(0, node.style.border_radius[0]);
140
+ const int tr = std::max<int>(0, node.style.border_radius[1]);
141
+ const int br = std::max<int>(0, node.style.border_radius[2]);
142
+ const int bl = std::max<int>(0, node.style.border_radius[3]);
143
+ const CGFloat radius = (tl == tr && tr == br && br == bl)
144
+ ? static_cast<CGFloat>(tl)
145
+ : static_cast<CGFloat>(tl + tr + br + bl) / 4.0;
146
+ view.layer.cornerRadius = radius * scale;
147
+ view.clipsToBounds = view.layer.cornerRadius > 0 || node.style.overflow == 1 || node.style.overflow == 2;
148
+ }
149
+
150
+ void applyImageProps(UIImageView *imageView, const gea::embedded::ui::Node &node)
151
+ {
152
+ NSNumber *currentId = objc_getAssociatedObject(imageView, "gea.image_id");
153
+ if (!currentId || currentId.intValue != node.image_id) {
154
+ imageView.image = gea::ios::imageForId(node.image_id);
155
+ objc_setAssociatedObject(imageView, "gea.image_id", @(node.image_id), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
156
+ }
157
+ switch (node.style.image_fit) {
158
+ case 1: imageView.contentMode = UIViewContentModeScaleAspectFit; break;
159
+ case 2: imageView.contentMode = UIViewContentModeScaleAspectFill; break;
160
+ case 3: imageView.contentMode = UIViewContentModeCenter; break;
161
+ case 4: imageView.contentMode = UIViewContentModeScaleAspectFit; break;
162
+ default: imageView.contentMode = UIViewContentModeScaleToFill;
163
+ }
164
+ imageView.clipsToBounds = YES;
165
+ }
166
+
167
+ void applyCanvasProps(GeaCanvasView *canvasView, int nodeId)
168
+ {
169
+ canvasView.nodeId = nodeId;
170
+ [canvasView setNeedsDisplay];
171
+ }
172
+
173
+ void applyTypeSpecificProps(UIView *view, const gea::embedded::ui::Node &node, int nodeId, CGFloat scale)
174
+ {
175
+ using gea::embedded::ui::NodeType;
176
+ if ([view isKindOfClass:[GeaNativeScrollContainer class]]) {
177
+ applyScrollProps((GeaNativeScrollContainer *)view, node, nodeId, scale);
178
+ } else if (node.type == NodeType::Button) {
179
+ applyButtonProps((GeaNativeButton *)view, node, nodeId, scale);
180
+ } else if (node.type == NodeType::Text) {
181
+ applyTextProps((GeaNativeLabel *)view, node, scale);
182
+ } else if (node.type == NodeType::Image) {
183
+ applyImageProps((UIImageView *)view, node);
184
+ } else if (node.type == NodeType::Canvas) {
185
+ applyCanvasProps((GeaCanvasView *)view, nodeId);
186
+ }
187
+ }
188
+
189
+ UIView *ensureViewForNode(int nodeId, const gea::embedded::ui::Node &node)
190
+ {
191
+ auto &tree = gea::embedded::ui::Tree::instance();
192
+ NSNumber *key = @(nodeId);
193
+ UIView *view = nodeIdToView()[key];
194
+ const char *tagName = tree.tagName(nodeId);
195
+ if (view && !viewMatchesNode(view, node, tagName)) {
196
+ [view removeFromSuperview];
197
+ [nodeIdToView() removeObjectForKey:key];
198
+ view = nil;
199
+ }
200
+ if (view) return view;
201
+ view = makeViewForType(node.type, tagName, node);
202
+ if (!view) return nil;
203
+ objc_setAssociatedObject(view, "gea.node_id", key, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
204
+ nodeIdToView()[key] = view;
205
+ return view;
206
+ }
207
+
208
+ void syncRecursive(int nodeId, UIView *parent, int parentAbsX, int parentAbsY, CGFloat scale,
209
+ NSMutableSet<NSNumber *> *unseen)
210
+ {
211
+ using namespace gea::embedded::ui;
212
+ Tree &tree = Tree::instance();
213
+ if (nodeId < 0 || nodeId >= tree.nodeCount()) return;
214
+ const Node &node = tree.node(nodeId);
215
+ const char *tagName = tree.tagName(nodeId);
216
+ if (node.type == NodeType::View && tagName && std::strcmp(tagName, "input") == 0) return;
217
+
218
+ NSNumber *key = @(nodeId);
219
+ [unseen removeObject:key];
220
+ UIView *view = ensureViewForNode(nodeId, node);
221
+ if (!view) return;
222
+ if (view.superview != parent) {
223
+ [view removeFromSuperview];
224
+ [parent addSubview:view];
225
+ }
226
+ applyViewStyle(view, node, parentAbsX, parentAbsY, scale);
227
+ applyTypeSpecificProps(view, node, nodeId, scale);
228
+
229
+ UIView *parentForChildren = view;
230
+ int parentAbsXForChildren = node.layout.x;
231
+ int parentAbsYForChildren = node.layout.y;
232
+ if ([view isKindOfClass:[GeaNativeScrollContainer class]]) {
233
+ parentForChildren = ((GeaNativeScrollContainer *)view).contentView;
234
+ parentAbsYForChildren = node.layout.y - tree.scrollTop(nodeId);
235
+ }
236
+
237
+ for (int child = node.first_child; child >= 0; child = tree.node(child).next_sibling) {
238
+ syncRecursive(child, parentForChildren, parentAbsXForChildren, parentAbsYForChildren, scale, unseen);
239
+ }
240
+ }
241
+
242
+ } // namespace
243
+
244
+ void syncNativeTree(UIView *parentForRoot, int rootNodeId)
245
+ {
246
+ using namespace gea::embedded::ui;
247
+ Tree &tree = Tree::instance();
248
+ if (!parentForRoot || rootNodeId < 0 || rootNodeId >= tree.nodeCount()) return;
249
+
250
+ NSMutableSet<NSNumber *> *unseen = [NSMutableSet setWithArray:[nodeIdToView() allKeys]];
251
+ const CGFloat scale = canvasScaleForView(parentForRoot);
252
+ syncRecursive(rootNodeId, parentForRoot, 0, 0, scale, unseen);
253
+
254
+ for (NSNumber *gone in unseen) {
255
+ UIView *view = nodeIdToView()[gone];
256
+ [view removeFromSuperview];
257
+ [nodeIdToView() removeObjectForKey:gone];
258
+ }
259
+ }
260
+
261
+ void teardownNativeTree()
262
+ {
263
+ for (UIView *view in [nodeIdToView() allValues]) {
264
+ [view removeFromSuperview];
265
+ }
266
+ [nodeIdToView() removeAllObjects];
267
+ }
268
+
269
+ bool hasNativeTree()
270
+ {
271
+ return nodeIdToView().count > 0;
272
+ }
273
+
274
+ } // namespace gea::ios::renderer
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key><string>en</string>
6
+ <key>CFBundleExecutable</key><string>@APP_EXEC@</string>
7
+ <key>CFBundleIdentifier</key><string>com.gea.@APP_ID@</string>
8
+ <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
9
+ <key>CFBundleName</key><string>@APP_NAME@</string>
10
+ <key>CFBundleIconFile</key><string>AppIcon</string>
11
+ <key>CFBundlePackageType</key><string>APPL</string>
12
+ <key>CFBundleShortVersionString</key><string>0.1.0</string>
13
+ <key>CFBundleVersion</key><string>1</string>
14
+ <key>LSMinimumSystemVersion</key><string>11.0</string>
15
+ <key>NSHighResolutionCapable</key><true/>
16
+ <key>NSPrincipalClass</key><string>NSApplication</string>
17
+ <!-- The fetch facade rides NSURLSession, which enforces App Transport
18
+ Security. Framework apps talk to plain-http endpoints (LAN devices,
19
+ local dev servers) exactly as they do on the ESP32 and raspberry-pi-os
20
+ targets, where no ATS exists — allow those loads for parity. -->
21
+ <key>NSAppTransportSecurity</key>
22
+ <dict>
23
+ <key>NSAllowsArbitraryLoads</key><true/>
24
+ </dict>
25
+ </dict>
26
+ </plist>
@@ -0,0 +1,131 @@
1
+ # macOS target
2
+
3
+ Build a gea-embedded app as a native Cocoa Mac app — `<Button>` becomes
4
+ `NSButton`, `<Text>` becomes `NSTextField`, `<View>` becomes `NSView`,
5
+ `<Image>` becomes `NSImageView`, `<canvas>` is a custom `NSView` backed by
6
+ CoreGraphics, `<virtual-list>` is `NSScrollView`. The flex layout engine
7
+ in `@geastack/engine`'s `ui/layout.cpp` computes frames; AppKit draws.
8
+
9
+ ## Build
10
+
11
+ targets/macos/build-macos.sh <app-id>
12
+ open dist/macos/<app-id>/<AppName>.app
13
+
14
+ ## Where the build writes
15
+
16
+ Everything this target produces lands under the **app project** being built,
17
+ never under this package: `<app>/dist/macos/<app-id>/<AppName>.app` for the
18
+ bundle, `<app>/dist/macos/<app-id>/build/` for objects and PCHs, and
19
+ `<app>/dist/macos/.generated/<app-id>/` for the generated C++. The app is the
20
+ directory the build runs in: `gea build` runs this script in the app, and a
21
+ direct run uses the invocation directory.
22
+
23
+ This package is installed into an app's `node_modules`, so a package-relative
24
+ output path hid every build inside a directory nobody opens and npm deletes on
25
+ the next install. Set `GEA_MACOS_OUTPUT_DIR` to put the whole tree elsewhere.
26
+
27
+ `<app-id>` is currently any name; when no generated app is present under
28
+ `dist/macos/.generated/<app-id>/` the build falls back to a built-in
29
+ Phase B smoke app (`macos_smoke_app.cpp`) that exercises View / Text /
30
+ Button / event dispatch. Real geatsc-compiled apps drop into
31
+ `dist/macos/.generated/` and supersede the smoke automatically — the
32
+ JSX → C++ pipeline for the macOS target is a follow-up.
33
+
34
+ ## Hard-muted builds must prove artifact provenance
35
+
36
+ For any verification that must not produce audio, configure the final native
37
+ mute source and its literal on the build itself:
38
+
39
+ ```sh
40
+ GEA_MACOS_HARD_MUTE_SOURCE=/absolute/path/to/the/committed/native/audio_source.mm \
41
+ GEA_MACOS_HARD_MUTE_GATE=THE_NATIVE_MUTE_ENVIRONMENT_LITERAL \
42
+ targets/macos/build-macos.sh <app-id>
43
+ ```
44
+
45
+ Both variables are required together. After code signing, the build writes
46
+ `dist/<app-id>/build/hard-mute-artifact-provenance.json` and statically proves:
47
+
48
+ - the selected source is the committed blob at the recorded dependency revision;
49
+ - exactly one linked object depends on that source and contains the gate;
50
+ - `link.sig` records that exact object;
51
+ - source, translation unit, depfile, link signature, object, and executable
52
+ SHA-256 values still match; and
53
+ - the signed executable contains the same gate.
54
+
55
+ Immediately before launch, rerun the no-launch check against the exact binary:
56
+
57
+ ```sh
58
+ node targets/macos/preflight-hard-muted-artifact.mjs verify \
59
+ --manifest /absolute/path/to/build/hard-mute-artifact-provenance.json \
60
+ --executable "/absolute/path/to/App.app/Contents/MacOS/App" \
61
+ --source /absolute/path/to/the/committed/native/audio_source.mm \
62
+ --gate THE_NATIVE_MUTE_ENVIRONMENT_LITERAL
63
+ ```
64
+
65
+ Do not launch unless this prints
66
+ `PASS artifact-static-hard-mute-gate=present`. The checker reads files and Git
67
+ metadata only; it does not execute the app or open an audio device. Any missing
68
+ manifest, gate, changed dependency revision, or changed source/object/executable
69
+ fails closed.
70
+
71
+ ## Architecture
72
+
73
+ JSX (or smoke C++) → Document / Tree mutations → Tree::computeLayout
74
+ → MacosRenderer::sync (walks the laid-out tree)
75
+ → ensureViewForNode dispatches on NodeType
76
+ → NSView / NSTextField / NSButton / NSImageView / GeaCanvasView
77
+ / NSScrollView
78
+ → applyViewStyle (frame, background, border, cornerRadius,
79
+ opacity, rotation, masksToBounds)
80
+ → applyTypeSpecificProps (per-type content)
81
+
82
+ The display-list pipeline (`ui/render.cpp`, `ui/dirty_regions.cpp`) is
83
+ bypassed on this target — AppKit owns its own invalidation. The
84
+ custom touch/momentum code in `ui/input.cpp` is bypassed too;
85
+ NSClickGestureRecognizer (on Views/anything with a pressId) and
86
+ NSButton target/action dispatch Click+Press events back into
87
+ `Tree::dispatchEvent`. NSScrollView gives VirtualList native momentum
88
+ and elastic boundaries.
89
+
90
+ The only intrusive change to the framework is `Tree::computeLayout`
91
+ (peer of `refresh`) — it runs just the flex pass without building a
92
+ display list, so other targets keep calling `refresh()` unchanged.
93
+
94
+ ## Frame loop
95
+
96
+ `macos_main.mm` schedules a 60Hz `NSTimer` on `NSRunLoopCommonModes` so
97
+ the loop keeps firing during live window-resize and other event-
98
+ tracking modes. Each tick:
99
+
100
+ 1. `gea_embedded_app_frame(timestampMs)` lets the app update state
101
+ 2. Force `style.width/height` on the mounted root to the current window
102
+ content size — this is the macOS convention that makes the root
103
+ viewport track resize automatically
104
+ 3. `Tree::computeLayout(root, w, h)`
105
+ 4. `MacosRenderer::sync(rootView, root)`
106
+
107
+ ## Verification stub
108
+
109
+ Running with `GEA_MACOS_VERIFY_ONCE=1` logs the tree, NSView hierarchy,
110
+ fires a press on the first Button node twice, then exits. Useful for
111
+ quick regression checks after changes to the renderer.
112
+
113
+ ## What works / what's stubbed
114
+
115
+ Native: View / Text / Button / Image / Canvas / VirtualList; flex
116
+ layout; click + press events; trackpad-momentum scroll; window resize
117
+ (live + final); standard menu bar with Cmd-Q.
118
+
119
+ Audio is real: `targets/shared/apple_audio.mm` (shared with iOS) backs
120
+ `gea::platform::audio` with AVAudioEngine — sample-accurately scheduled,
121
+ mixed oscillators on an `AVAudioSourceNode`, plus `playFile`/`playPcm` on
122
+ `AVAudioPlayerNode`s. `targets/shared/build-audio-selftest.sh` builds and
123
+ runs the measurement harness (offline render, RMS + measured frequency);
124
+ `GEA_AUDIO_CAPTURE_RAW=<path>` captures a real app run for
125
+ `targets/shared/analyze-audio-capture.mjs`.
126
+
127
+ Stubbed for v1: WiFi (`macos_wifi`), BLE (`macos_ble`), IMU (`macos_imu`)
128
+ — present only as link-satisfying no-ops; apps that depend on these will
129
+ run but those calls return inert values. Custom font bundling, code
130
+ signing with entitlements, notarisation, and the JSX → C++ pipeline for
131
+ this target are follow-ups.
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Child-process lifecycle support for build-macos.sh. Keep this Bash 3.2
4
+ # compatible: macOS still ships that version as /bin/bash.
5
+ ACTIVE_BUILD_CHILD_PID=""
6
+ PIDS=()
7
+
8
+ macos_run_tracked_child() {
9
+ if [[ -n "$ACTIVE_BUILD_CHILD_PID" ]]; then
10
+ echo "Internal error: macOS build child $ACTIVE_BUILD_CHILD_PID is still active" >&2
11
+ return 1
12
+ fi
13
+
14
+ "$@" &
15
+ ACTIVE_BUILD_CHILD_PID="$!"
16
+ local child_pid="$ACTIVE_BUILD_CHILD_PID"
17
+ local status=0
18
+ if wait "$child_pid"; then
19
+ status=0
20
+ else
21
+ status="$?"
22
+ fi
23
+ ACTIVE_BUILD_CHILD_PID=""
24
+ return "$status"
25
+ }
26
+
27
+ macos_build_process_tree() {
28
+ local roots="$1"
29
+ if [[ -n "${MACOS_BUILD_PROCESS_TABLE_FILE:-}" ]]; then
30
+ cat "$MACOS_BUILD_PROCESS_TABLE_FILE"
31
+ else
32
+ ps -axo pid=,ppid= 2>/dev/null
33
+ fi | awk -v roots="$roots" '
34
+ BEGIN {
35
+ count = split(roots, root, " ")
36
+ for (i = 1; i <= count; i++) {
37
+ if (root[i] ~ /^[0-9]+$/) wanted[root[i]] = 1
38
+ }
39
+ }
40
+ {
41
+ pid[NR] = $1
42
+ parent[NR] = $2
43
+ }
44
+ END {
45
+ changed = 1
46
+ while (changed) {
47
+ changed = 0
48
+ for (i = 1; i <= NR; i++) {
49
+ if ((parent[i] in wanted) && wanted[parent[i]] == 1 && !(pid[i] in wanted)) {
50
+ wanted[pid[i]] = 1
51
+ changed = 1
52
+ }
53
+ }
54
+ }
55
+ for (candidate in wanted) {
56
+ if (wanted[candidate] == 1) print candidate
57
+ }
58
+ }
59
+ '
60
+ }
61
+
62
+ macos_build_pid_is_running() {
63
+ local pid="$1"
64
+ local state=""
65
+ kill -0 "$pid" 2>/dev/null || return 1
66
+ state="$(ps -o stat= -p "$pid" 2>/dev/null | awk 'NR == 1 { print $1 }')"
67
+ [[ -n "$state" && "$state" != Z* ]]
68
+ }
69
+
70
+ macos_terminate_and_reap_build_children() {
71
+ local roots=()
72
+ local tree=()
73
+ local root_list=""
74
+ local pid=""
75
+ local duplicate=0
76
+ local attempt=0
77
+ local any_running=0
78
+
79
+ if [[ "$ACTIVE_BUILD_CHILD_PID" =~ ^[0-9]+$ ]]; then
80
+ roots+=("$ACTIVE_BUILD_CHILD_PID")
81
+ fi
82
+ for pid in ${PIDS[@]+"${PIDS[@]}"}; do
83
+ [[ "$pid" =~ ^[0-9]+$ ]] || continue
84
+ duplicate=0
85
+ local existing=""
86
+ for existing in ${roots[@]+"${roots[@]}"}; do
87
+ if [[ "$existing" == "$pid" ]]; then duplicate=1; break; fi
88
+ done
89
+ if (( duplicate == 0 )); then roots+=("$pid"); fi
90
+ done
91
+ (( ${#roots[@]} > 0 )) || return 0
92
+
93
+ root_list="${roots[*]}"
94
+ while IFS= read -r pid; do
95
+ [[ "$pid" =~ ^[0-9]+$ ]] && tree+=("$pid")
96
+ done < <(macos_build_process_tree "$root_list")
97
+
98
+ # Signal the captured descendants and their direct build parents while the
99
+ # app-local lock is still held. The second pass escalates stubborn compiler
100
+ # processes; all directly-owned children are then waited to avoid zombies.
101
+ for pid in ${tree[@]+"${tree[@]}"}; do kill -TERM "$pid" 2>/dev/null || true; done
102
+ attempt=0
103
+ while (( attempt < 20 )); do
104
+ any_running=0
105
+ for pid in ${tree[@]+"${tree[@]}"}; do
106
+ if macos_build_pid_is_running "$pid"; then any_running=1; break; fi
107
+ done
108
+ (( any_running != 0 )) || break
109
+ sleep 0.05
110
+ attempt=$((attempt + 1))
111
+ done
112
+ for pid in ${tree[@]+"${tree[@]}"}; do
113
+ if macos_build_pid_is_running "$pid"; then kill -KILL "$pid" 2>/dev/null || true; fi
114
+ done
115
+ for pid in ${roots[@]+"${roots[@]}"}; do wait "$pid" 2>/dev/null || true; done
116
+
117
+ ACTIVE_BUILD_CHILD_PID=""
118
+ PIDS=()
119
+ }