@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.
- package/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +3076 -0
- package/generated/AVFoundation.d.ts +112 -0
- package/generated/ActivityKit.d.ts +7 -0
- package/generated/AppKit.d.ts +268 -0
- package/generated/CoreGraphics.d.ts +18 -0
- package/generated/CoreLocation.d.ts +28 -0
- package/generated/CoreMedia.d.ts +8 -0
- package/generated/Dispatch.d.ts +2 -0
- package/generated/Foundation.d.ts +13 -0
- package/generated/MapKit.d.ts +28 -0
- package/generated/Metal.d.ts +91 -0
- package/generated/MetalKit.d.ts +22 -0
- package/generated/Photos.d.ts +5 -0
- package/generated/QuartzCore.d.ts +12 -0
- package/generated/SwiftData.d.ts +11 -0
- package/generated/UIKit.d.ts +187 -0
- package/package.json +123 -0
- package/runtime/AVFoundation.js +136 -0
- package/runtime/ActivityKit.js +18 -0
- package/runtime/AppKit.js +313 -0
- package/runtime/CoreGraphics.js +15 -0
- package/runtime/CoreLocation.js +30 -0
- package/runtime/CoreMedia.js +7 -0
- package/runtime/Dispatch.js +11 -0
- package/runtime/Foundation.js +21 -0
- package/runtime/MapKit.js +22 -0
- package/runtime/Metal.js +126 -0
- package/runtime/MetalKit.js +15 -0
- package/runtime/Photos.js +9 -0
- package/runtime/QuartzCore.js +9 -0
- package/runtime/SwiftData.js +29 -0
- package/runtime/UIKit.js +242 -0
- package/runtime/empty.js +1 -0
- package/targets/ios/Info.plist.in +25 -0
- package/targets/ios/README.md +46 -0
- package/targets/ios/build-ios.sh +395 -0
- package/targets/ios/detect-development-team.mjs +109 -0
- package/targets/ios/generate-resident-entry.mjs +67 -0
- package/targets/ios/generate-resident-registry.mjs +162 -0
- package/targets/ios/generate-xcode-project.mjs +520 -0
- package/targets/ios/main/canvas_view.h +7 -0
- package/targets/ios/main/canvas_view.mm +94 -0
- package/targets/ios/main/font_registry.h +17 -0
- package/targets/ios/main/font_registry.mm +129 -0
- package/targets/ios/main/image_bridge.h +13 -0
- package/targets/ios/main/image_bridge.mm +53 -0
- package/targets/ios/main/ios_apps.c +28 -0
- package/targets/ios/main/ios_camera.mm +446 -0
- package/targets/ios/main/ios_display.mm +344 -0
- package/targets/ios/main/ios_main.mm +811 -0
- package/targets/ios/main/ios_memory.cpp +55 -0
- package/targets/ios/main/ios_renderer.h +20 -0
- package/targets/ios/main/ios_renderer.mm +27 -0
- package/targets/ios/main/ios_root_background.cpp +20 -0
- package/targets/ios/main/ios_root_background.h +11 -0
- package/targets/ios/main/ios_sensors.mm +244 -0
- package/targets/ios/main/ios_timers.mm +53 -0
- package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
- package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
- package/targets/ios/main/renderer/native_button.mm +179 -0
- package/targets/ios/main/renderer/native_label.mm +161 -0
- package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
- package/targets/ios/main/renderer/view_reconciler.mm +274 -0
- package/targets/macos/Info.plist.in +26 -0
- package/targets/macos/README.md +131 -0
- package/targets/macos/build-child-lifecycle.sh +119 -0
- package/targets/macos/build-macos.sh +2156 -0
- package/targets/macos/check-module-graph-freshness.mjs +82 -0
- package/targets/macos/compiler-input-fingerprint.mjs +32 -0
- package/targets/macos/finalize-apple-native-output.mjs +77 -0
- package/targets/macos/generate-resident-entry.mjs +80 -0
- package/targets/macos/generate-resident-registry.mjs +141 -0
- package/targets/macos/heavy-build-lock.sh +176 -0
- package/targets/macos/main/canvas_view.h +9 -0
- package/targets/macos/main/canvas_view.mm +176 -0
- package/targets/macos/main/color_convert.h +18 -0
- package/targets/macos/main/color_convert.mm +39 -0
- package/targets/macos/main/font_registry.h +35 -0
- package/targets/macos/main/font_registry.mm +350 -0
- package/targets/macos/main/image_bridge.h +16 -0
- package/targets/macos/main/image_bridge.mm +52 -0
- package/targets/macos/main/macos_app_platform.mm +95 -0
- package/targets/macos/main/macos_apps.c +31 -0
- package/targets/macos/main/macos_device_control.mm +56 -0
- package/targets/macos/main/macos_display.mm +484 -0
- package/targets/macos/main/macos_host_device_control.mm +95 -0
- package/targets/macos/main/macos_main.mm +839 -0
- package/targets/macos/main/macos_memory.cpp +31 -0
- package/targets/macos/main/macos_native_shell.h +31 -0
- package/targets/macos/main/macos_native_shell.mm +396 -0
- package/targets/macos/main/macos_network.mm +253 -0
- package/targets/macos/main/macos_renderer.h +39 -0
- package/targets/macos/main/macos_renderer.mm +1969 -0
- package/targets/macos/main/macos_sensors.cpp +148 -0
- package/targets/macos/main/macos_smoke_app.cpp +65 -0
- package/targets/macos/main/macos_timers.mm +52 -0
- package/targets/macos/main/press_bridge.h +21 -0
- package/targets/macos/main/press_bridge.mm +72 -0
- package/targets/macos/main/thermalright_hid_display.h +14 -0
- package/targets/macos/main/thermalright_hid_display.mm +995 -0
- package/targets/macos/native-artifact-provenance.mjs +421 -0
- package/targets/macos/order-compile-edges.mjs +64 -0
- package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
- package/targets/macos/prune-generated-modules.mjs +147 -0
- package/targets/macos/sync-resources.mjs +153 -0
- package/targets/shared/analyze-audio-capture.mjs +131 -0
- package/targets/shared/apple_audio.mm +1194 -0
- package/targets/shared/apple_audio_selftest.mm +602 -0
- package/targets/shared/apple_audio_testing.h +34 -0
- 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
|
+
}
|