@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,179 @@
1
+ #include "ios_renderer_internal.h"
2
+
3
+ #include "events.h"
4
+ #include "font_registry.h"
5
+ #include "ui/tree_internal.h"
6
+
7
+ #include <algorithm>
8
+ #include <cmath>
9
+
10
+ extern "C" void gea_ios_touch_set_state(int touching, int x, int y);
11
+
12
+ @implementation GeaNativeButton
13
+
14
+ - (CGPoint)eventPoint
15
+ {
16
+ auto &tree = gea::embedded::ui::Tree::instance();
17
+ if (self.nodeId < 0 || self.nodeId >= tree.nodeCount()) return CGPointZero;
18
+ const auto &node = tree.node(self.nodeId);
19
+ return CGPointMake(static_cast<CGFloat>(node.layout.x + node.layout.width / 2),
20
+ static_cast<CGFloat>(node.layout.y + node.layout.height / 2));
21
+ }
22
+
23
+ - (bool)dispatchPointerEvent:(gea::framework::events::PointerEventType)type activeTouch:(BOOL)activeTouch
24
+ {
25
+ if (self.nodeId < 0) return false;
26
+ auto &tree = gea::embedded::ui::Tree::instance();
27
+ if (self.nodeId >= tree.nodeCount()) return false;
28
+
29
+ const CGPoint point = [self eventPoint];
30
+ gea::framework::events::PointerEvent event{};
31
+ event.type = type;
32
+ event.targetId = self.nodeId;
33
+ event.pointerId = 1;
34
+ event.x = static_cast<int>(std::lround(point.x));
35
+ event.y = static_cast<int>(std::lround(point.y));
36
+ event.clientX = event.x;
37
+ event.clientY = event.y;
38
+ event.pageX = event.x;
39
+ event.pageY = event.y;
40
+ event.screenX = event.x;
41
+ event.screenY = event.y;
42
+ event.primary = true;
43
+ event.touchesLength = activeTouch ? 1 : 0;
44
+ event.targetTouchesLength = activeTouch ? 1 : 0;
45
+ event.changedTouchesLength = 1;
46
+ event.touches[0].identifier = 1;
47
+ event.touches[0].target = gea::framework::events::EventTarget(self.nodeId);
48
+ event.touches[0].screenX = event.x;
49
+ event.touches[0].screenY = event.y;
50
+ event.touches[0].clientX = event.x;
51
+ event.touches[0].clientY = event.y;
52
+ event.touches[0].pageX = event.x;
53
+ event.touches[0].pageY = event.y;
54
+ event.targetTouches[0] = event.touches[0];
55
+ event.changedTouches[0] = event.touches[0];
56
+ tree.dispatchEvent(event);
57
+ return event.defaultPrevented;
58
+ }
59
+
60
+ - (void)setNativeTouchState:(BOOL)touching
61
+ {
62
+ const CGPoint point = [self eventPoint];
63
+ gea_ios_touch_set_state(touching ? 1 : 0,
64
+ static_cast<int>(std::lround(point.x)),
65
+ static_cast<int>(std::lround(point.y)));
66
+ }
67
+
68
+ - (void)handleTouchDown:(id)sender
69
+ {
70
+ (void)sender;
71
+ auto &tree = gea::embedded::ui::Tree::instance();
72
+ if (self.nodeId < 0 || self.nodeId >= tree.nodeCount()) return;
73
+ const CGPoint point = [self eventPoint];
74
+ const int x = static_cast<int>(std::lround(point.x));
75
+ const int y = static_cast<int>(std::lround(point.y));
76
+ [self setNativeTouchState:YES];
77
+ tree.pointerDown(x, y);
78
+ [self dispatchPointerEvent:gea::framework::events::PointerEventType::TouchStart activeTouch:YES];
79
+ }
80
+
81
+ - (void)handleTouchDrag:(id)sender
82
+ {
83
+ (void)sender;
84
+ auto &tree = gea::embedded::ui::Tree::instance();
85
+ if (self.nodeId < 0 || self.nodeId >= tree.nodeCount()) return;
86
+ const CGPoint point = [self eventPoint];
87
+ const int x = static_cast<int>(std::lround(point.x));
88
+ const int y = static_cast<int>(std::lround(point.y));
89
+ [self setNativeTouchState:YES];
90
+ [self dispatchPointerEvent:gea::framework::events::PointerEventType::TouchMove activeTouch:YES];
91
+ tree.pointerMove(x, y);
92
+ }
93
+
94
+ - (void)finishTouchInside:(BOOL)inside
95
+ {
96
+ if (self.nodeId < 0) return;
97
+ auto &tree = gea::embedded::ui::Tree::instance();
98
+ if (self.nodeId >= tree.nodeCount()) return;
99
+ [self setNativeTouchState:NO];
100
+ tree.pointerUp();
101
+ [self dispatchPointerEvent:gea::framework::events::PointerEventType::TouchEnd activeTouch:NO];
102
+ if (inside) {
103
+ [self dispatchPointerEvent:gea::framework::events::PointerEventType::Click activeTouch:NO];
104
+ }
105
+ }
106
+
107
+ - (void)handleTouchUpInside:(id)sender
108
+ {
109
+ (void)sender;
110
+ [self finishTouchInside:YES];
111
+ }
112
+
113
+ - (void)handleTouchUpOutside:(id)sender
114
+ {
115
+ (void)sender;
116
+ [self finishTouchInside:NO];
117
+ }
118
+
119
+ - (void)handleTouchCancel:(id)sender
120
+ {
121
+ (void)sender;
122
+ [self finishTouchInside:NO];
123
+ }
124
+
125
+ @end
126
+
127
+ namespace gea::ios::renderer {
128
+ namespace {
129
+
130
+ const gea::embedded::ui::Node *firstVisibleTextChild(int nodeId)
131
+ {
132
+ using gea::embedded::ui::NodeType;
133
+ auto &tree = gea::embedded::ui::Tree::instance();
134
+ if (nodeId < 0 || nodeId >= tree.nodeCount()) return nullptr;
135
+ const auto &buttonNode = tree.node(nodeId);
136
+ for (int child = buttonNode.first_child; child >= 0; child = tree.node(child).next_sibling) {
137
+ const auto &childNode = tree.node(child);
138
+ if (childNode.type == NodeType::Text && childNode.style.display != 1) return &childNode;
139
+ }
140
+ return nullptr;
141
+ }
142
+
143
+ NSAttributedString *attributedTitleForTextChild(const gea::embedded::ui::Node &textNode, CGFloat scale)
144
+ {
145
+ NSString *raw = NSStringFromText(textNode.text);
146
+ const CGFloat fontSize = std::max<CGFloat>(1.0, static_cast<CGFloat>(textNode.style.font_size > 0 ? textNode.style.font_size : 16) * scale);
147
+ UIFont *font = gea::ios::fontForId(textNode.style.font_id, fontSize);
148
+ UIColor *color = rgb565ToUIColor(textNode.style.text_color);
149
+ NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
150
+ paragraph.alignment = textAlignmentForStyle(textNode.style.text_align);
151
+ paragraph.lineBreakMode = NSLineBreakByWordWrapping;
152
+ NSMutableDictionary *attrs = textAttributes(font, color, textNode.style.text_decoration);
153
+ attrs[NSParagraphStyleAttributeName] = paragraph;
154
+ return [[NSAttributedString alloc] initWithString:raw attributes:attrs];
155
+ }
156
+
157
+ } // namespace
158
+
159
+ void applyButtonProps(GeaNativeButton *button, const gea::embedded::ui::Node &node, int nodeId, CGFloat scale)
160
+ {
161
+ button.nodeId = nodeId;
162
+ button.userInteractionEnabled = !(node.style.display == 1 || node.style.opacity == 0 ||
163
+ node.layout.width <= 0 || node.layout.height <= 0);
164
+ button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
165
+ button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
166
+ button.titleLabel.numberOfLines = 0;
167
+ button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
168
+
169
+ const gea::embedded::ui::Node *titleNode = firstVisibleTextChild(nodeId);
170
+ if (titleNode && !titleNode->text.empty()) {
171
+ button.titleLabel.hidden = NO;
172
+ [button setAttributedTitle:attributedTitleForTextChild(*titleNode, scale) forState:UIControlStateNormal];
173
+ } else {
174
+ button.titleLabel.hidden = YES;
175
+ [button setAttributedTitle:nil forState:UIControlStateNormal];
176
+ }
177
+ }
178
+
179
+ } // namespace gea::ios::renderer
@@ -0,0 +1,161 @@
1
+ #include "ios_renderer_internal.h"
2
+
3
+ #include "font_registry.h"
4
+ #include "graphics/font.h"
5
+
6
+ #include <algorithm>
7
+ #include <cmath>
8
+ #include <string>
9
+ #include <vector>
10
+
11
+ @implementation GeaNativeLabel
12
+
13
+ - (void)drawRect:(CGRect)rect
14
+ {
15
+ if (!self.geaUseBitmapFont) {
16
+ [super drawRect:rect];
17
+ return;
18
+ }
19
+ (void)rect;
20
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
21
+ if (!ctx) return;
22
+
23
+ const CGFloat pixel = std::max<CGFloat>(1.0, self.geaBitmapGlyphScale);
24
+ const CGFloat glyphW = gea::framework::graphics::BitmapFont8x16::kWidth * pixel;
25
+ const CGFloat glyphH = gea::framework::graphics::BitmapFont8x16::kHeight * pixel;
26
+ if (glyphW <= 0 || glyphH <= 0) return;
27
+
28
+ std::string text = self.geaBitmapText ? self.geaBitmapText.UTF8String : "";
29
+ const std::size_t maxChars = std::max<std::size_t>(1, static_cast<std::size_t>(std::floor(self.bounds.size.width / glyphW)));
30
+ std::vector<std::string> lines;
31
+ std::string line;
32
+ for (unsigned char c : text) {
33
+ if (c == '\r') continue;
34
+ if (c == '\n') {
35
+ lines.push_back(line);
36
+ line.clear();
37
+ continue;
38
+ }
39
+ if (line.size() >= maxChars) {
40
+ lines.push_back(line);
41
+ line.clear();
42
+ }
43
+ line.push_back(c >= 0x20 && c <= 0x7e ? static_cast<char>(c) : '?');
44
+ }
45
+ lines.push_back(line);
46
+
47
+ UIColor *color = self.geaBitmapColor ?: UIColor.whiteColor;
48
+ [color setFill];
49
+ const auto &font = gea::framework::graphics::BitmapFont8x16::instance();
50
+ for (std::size_t lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
51
+ const std::string &textLine = lines[lineIndex];
52
+ const CGFloat lineWidth = static_cast<CGFloat>(textLine.size()) * glyphW;
53
+ CGFloat penX = 0;
54
+ if (self.textAlignment == NSTextAlignmentCenter) {
55
+ penX = std::max<CGFloat>(0, (self.bounds.size.width - lineWidth) / 2.0);
56
+ } else if (self.textAlignment == NSTextAlignmentRight) {
57
+ penX = std::max<CGFloat>(0, self.bounds.size.width - lineWidth);
58
+ }
59
+ const CGFloat lineStartX = penX;
60
+ const CGFloat penY = static_cast<CGFloat>(lineIndex) * glyphH;
61
+ if (penY >= self.bounds.size.height) break;
62
+ for (unsigned char c : textLine) {
63
+ const std::uint8_t *rows = font.glyphRows(static_cast<char>(c));
64
+ for (int row = 0; row < gea::framework::graphics::BitmapFont8x16::kHeight; row++) {
65
+ for (int col = 0; col < gea::framework::graphics::BitmapFont8x16::kWidth; col++) {
66
+ if ((rows[row] & (0x80 >> col)) == 0) continue;
67
+ CGContextFillRect(ctx, CGRectMake(penX + static_cast<CGFloat>(col) * pixel,
68
+ penY + static_cast<CGFloat>(row) * pixel,
69
+ pixel,
70
+ pixel));
71
+ }
72
+ }
73
+ penX += glyphW;
74
+ }
75
+ const CGFloat thickness = std::max<CGFloat>(1.0, std::ceil(pixel));
76
+ if (self.geaBitmapTextDecoration == 1) {
77
+ CGContextFillRect(ctx, CGRectMake(lineStartX, penY + glyphH * 0.88, lineWidth, thickness));
78
+ } else if (self.geaBitmapTextDecoration == 2) {
79
+ CGContextFillRect(ctx, CGRectMake(lineStartX, penY + glyphH * 0.58, lineWidth, thickness));
80
+ }
81
+ }
82
+ }
83
+
84
+ @end
85
+
86
+ namespace gea::ios::renderer {
87
+ namespace {
88
+
89
+ void fitNativeButtonLabelIfNeeded(GeaNativeLabel *label)
90
+ {
91
+ if (!label || ![label.superview isKindOfClass:[GeaNativeButton class]] || label.geaUseBitmapFont) return;
92
+ if (label.attributedText.length == 0) return;
93
+
94
+ const CGSize nativeSize = [label sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)];
95
+ if (nativeSize.width <= 0 || nativeSize.height <= 0) return;
96
+
97
+ UIView *button = label.superview;
98
+ const CGRect bounds = button.bounds;
99
+ CGRect frame = label.frame;
100
+ const CGFloat nextWidth = std::ceil(nativeSize.width) + 1.0;
101
+ const CGFloat nextHeight = std::ceil(nativeSize.height) + 1.0;
102
+ if (nextWidth > frame.size.width) {
103
+ const CGFloat centerX = CGRectGetMidX(frame);
104
+ frame.size.width = nextWidth;
105
+ frame.origin.x = centerX - nextWidth / 2.0;
106
+ if (bounds.size.width > 0 && nextWidth <= bounds.size.width) {
107
+ frame.origin.x = std::clamp<CGFloat>(frame.origin.x, 0, bounds.size.width - nextWidth);
108
+ }
109
+ }
110
+ if (nextHeight > frame.size.height) {
111
+ const CGFloat centerY = CGRectGetMidY(frame);
112
+ frame.size.height = nextHeight;
113
+ frame.origin.y = centerY - nextHeight / 2.0;
114
+ if (bounds.size.height > 0 && nextHeight <= bounds.size.height) {
115
+ frame.origin.y = std::clamp<CGFloat>(frame.origin.y, 0, bounds.size.height - nextHeight);
116
+ }
117
+ }
118
+ label.frame = frame;
119
+ }
120
+
121
+ } // namespace
122
+
123
+ void applyTextProps(GeaNativeLabel *label, const gea::embedded::ui::Node &node, CGFloat scale)
124
+ {
125
+ NSString *raw = NSStringFromText(node.text);
126
+ const CGFloat fontSize = std::max<CGFloat>(1.0, static_cast<CGFloat>(node.style.font_size > 0 ? node.style.font_size : 16) * scale);
127
+ UIColor *color = rgb565ToUIColor(node.style.text_color);
128
+ label.geaHostedByNativeButtonTitle = [label.superview isKindOfClass:[GeaNativeButton class]];
129
+ label.hidden = label.geaHostedByNativeButtonTitle;
130
+ label.textAlignment = textAlignmentForStyle(node.style.text_align);
131
+ if (node.style.font_id < 0) {
132
+ label.geaUseBitmapFont = YES;
133
+ label.geaBitmapText = raw;
134
+ label.geaBitmapColor = color;
135
+ label.geaBitmapGlyphScale = fontSize / gea::framework::graphics::BitmapFont8x16::kHeight;
136
+ label.geaBitmapTextDecoration = node.style.text_decoration;
137
+ label.attributedText = nil;
138
+ label.text = nil;
139
+ label.font = [UIFont systemFontOfSize:fontSize];
140
+ label.textColor = color;
141
+ [label setNeedsDisplay];
142
+ return;
143
+ }
144
+
145
+ label.geaUseBitmapFont = NO;
146
+ label.geaBitmapText = nil;
147
+ label.geaBitmapColor = nil;
148
+ UIFont *font = gea::ios::fontForId(node.style.font_id, fontSize);
149
+ NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
150
+ paragraph.alignment = textAlignmentForStyle(node.style.text_align);
151
+ paragraph.lineBreakMode = NSLineBreakByWordWrapping;
152
+ NSMutableDictionary *attrs = textAttributes(font, color, node.style.text_decoration);
153
+ attrs[NSParagraphStyleAttributeName] = paragraph;
154
+ label.attributedText = [[NSAttributedString alloc] initWithString:raw attributes:attrs];
155
+ label.font = font;
156
+ label.textColor = color;
157
+ fitNativeButtonLabelIfNeeded(label);
158
+ [label setNeedsDisplay];
159
+ }
160
+
161
+ } // namespace gea::ios::renderer
@@ -0,0 +1,279 @@
1
+ #include "ios_renderer_internal.h"
2
+
3
+ #include "ui/tree_internal.h"
4
+
5
+ #include <algorithm>
6
+ #include <cmath>
7
+ #include <cstdlib>
8
+ #include <cstring>
9
+
10
+ namespace gea::ios::renderer {
11
+ namespace {
12
+
13
+ bool scrollDebugMarkersEnabled()
14
+ {
15
+ static int cached = -1;
16
+ if (cached >= 0) return cached == 1;
17
+ bool enabled = false;
18
+ const char *env = std::getenv("GEA_IOS_SCROLL_DEBUG_MARKERS");
19
+ if (env && env[0] && std::strcmp(env, "0") != 0 && std::strcmp(env, "false") != 0) enabled = true;
20
+ for (NSString *arg in NSProcessInfo.processInfo.arguments) {
21
+ if ([arg isEqualToString:@"--gea-scroll-debug-markers"]) {
22
+ enabled = true;
23
+ break;
24
+ }
25
+ }
26
+ cached = enabled ? 1 : 0;
27
+ return enabled;
28
+ }
29
+
30
+ UILabel *makeScrollDebugLabel(NSString *identifier, UIColor *background)
31
+ {
32
+ UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
33
+ label.accessibilityIdentifier = identifier;
34
+ label.isAccessibilityElement = YES;
35
+ label.backgroundColor = background;
36
+ label.textColor = UIColor.whiteColor;
37
+ label.numberOfLines = 2;
38
+ label.font = [UIFont monospacedSystemFontOfSize:11.0 weight:UIFontWeightSemibold];
39
+ label.textAlignment = NSTextAlignmentLeft;
40
+ label.userInteractionEnabled = NO;
41
+ label.layer.cornerRadius = 4.0;
42
+ label.clipsToBounds = YES;
43
+ return label;
44
+ }
45
+
46
+ bool nearlyEqual(CGFloat a, CGFloat b)
47
+ {
48
+ return std::fabs(a - b) <= 0.5;
49
+ }
50
+
51
+ bool rectNearlyEqual(CGRect a, CGRect b)
52
+ {
53
+ return nearlyEqual(a.origin.x, b.origin.x) &&
54
+ nearlyEqual(a.origin.y, b.origin.y) &&
55
+ nearlyEqual(a.size.width, b.size.width) &&
56
+ nearlyEqual(a.size.height, b.size.height);
57
+ }
58
+
59
+ } // namespace
60
+ } // namespace gea::ios::renderer
61
+
62
+ @implementation GeaNativeScrollContainer
63
+
64
+ - (instancetype)initWithFrame:(CGRect)frame
65
+ {
66
+ self = [super initWithFrame:frame];
67
+ if (self) {
68
+ _nodeId = -1;
69
+ _syncingFromTree = NO;
70
+ _minObservedContentOffsetY = CGFLOAT_MAX;
71
+ _maxObservedContentOffsetY = -CGFLOAT_MAX;
72
+ self.delegate = self;
73
+ self.backgroundColor = UIColor.clearColor;
74
+ self.opaque = NO;
75
+ self.clipsToBounds = YES;
76
+ self.isAccessibilityElement = YES;
77
+ self.accessibilityIdentifier = @"gea-native-scroll";
78
+ self.bounces = YES;
79
+ self.alwaysBounceVertical = YES;
80
+ self.alwaysBounceHorizontal = NO;
81
+ self.directionalLockEnabled = YES;
82
+ self.decelerationRate = UIScrollViewDecelerationRateNormal;
83
+ self.showsVerticalScrollIndicator = NO;
84
+ self.showsHorizontalScrollIndicator = NO;
85
+ self.delaysContentTouches = NO;
86
+ self.canCancelContentTouches = YES;
87
+ self.scrollsToTop = NO;
88
+ if (@available(iOS 11.0, *)) self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
89
+ _contentView = [[UIView alloc] initWithFrame:CGRectZero];
90
+ _contentView.userInteractionEnabled = NO;
91
+ _contentView.backgroundColor = UIColor.clearColor;
92
+ [self addSubview:_contentView];
93
+ UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
94
+ tap.cancelsTouchesInView = NO;
95
+ [self addGestureRecognizer:tap];
96
+ }
97
+ return self;
98
+ }
99
+
100
+ - (void)ensureDebugMarkers
101
+ {
102
+ if (!gea::ios::renderer::scrollDebugMarkersEnabled()) return;
103
+ if (!self.debugTelemetryLabel) {
104
+ self.debugTelemetryLabel = gea::ios::renderer::makeScrollDebugLabel(
105
+ @"gea-scroll-telemetry", [UIColor colorWithRed:0.02 green:0.04 blue:0.08 alpha:0.86]);
106
+ [self addSubview:self.debugTelemetryLabel];
107
+ }
108
+ if (!self.debugTopMarkerLabel) {
109
+ self.debugTopMarkerLabel = gea::ios::renderer::makeScrollDebugLabel(
110
+ @"gea-scroll-content-top-marker", [UIColor colorWithRed:0.85 green:0.10 blue:0.10 alpha:0.82]);
111
+ self.debugTopMarkerLabel.text = @" content y=0";
112
+ [self.contentView addSubview:self.debugTopMarkerLabel];
113
+ }
114
+ if (!self.debugBottomMarkerLabel) {
115
+ self.debugBottomMarkerLabel = gea::ios::renderer::makeScrollDebugLabel(
116
+ @"gea-scroll-content-bottom-marker", [UIColor colorWithRed:0.10 green:0.42 blue:0.95 alpha:0.82]);
117
+ [self.contentView addSubview:self.debugBottomMarkerLabel];
118
+ }
119
+ }
120
+
121
+ - (void)layoutDebugMarkers
122
+ {
123
+ if (!gea::ios::renderer::scrollDebugMarkersEnabled()) return;
124
+ [self ensureDebugMarkers];
125
+ const CGFloat visibleX = self.contentOffset.x + 8.0;
126
+ const CGFloat visibleY = self.contentOffset.y + 8.0;
127
+ const CGFloat markerWidth = std::max<CGFloat>(160.0, self.bounds.size.width - 16.0);
128
+ self.debugTelemetryLabel.frame = CGRectMake(visibleX, visibleY, markerWidth, 42.0);
129
+ const CGFloat contentWidth = std::max<CGFloat>(self.contentSize.width, self.bounds.size.width);
130
+ self.debugTopMarkerLabel.frame = CGRectMake(0, 0, contentWidth, 24.0);
131
+ const CGFloat bottomY = std::max<CGFloat>(28.0, self.contentSize.height - 28.0);
132
+ self.debugBottomMarkerLabel.frame = CGRectMake(0, bottomY, contentWidth, 28.0);
133
+ [self bringSubviewToFront:self.debugTelemetryLabel];
134
+ }
135
+
136
+ - (void)layoutSubviews
137
+ {
138
+ [super layoutSubviews];
139
+ [self layoutDebugMarkers];
140
+ }
141
+
142
+ - (void)updateScrollTelemetryWithScale:(CGFloat)scale
143
+ {
144
+ if (self.nodeId < 0) return;
145
+ const CGFloat rawY = self.contentOffset.y;
146
+ if (rawY < self.minObservedContentOffsetY) self.minObservedContentOffsetY = rawY;
147
+ if (rawY > self.maxObservedContentOffsetY) self.maxObservedContentOffsetY = rawY;
148
+
149
+ auto &tree = gea::embedded::ui::Tree::instance();
150
+ const int treeY = self.nodeId < tree.nodeCount() ? tree.scrollTop(self.nodeId) : 0;
151
+ const CGFloat maxY = std::max<CGFloat>(0, self.contentSize.height - self.bounds.size.height);
152
+ const CGFloat overTop = std::min<CGFloat>(0, rawY);
153
+ const CGFloat overBottom = std::max<CGFloat>(0, rawY - maxY);
154
+ const CGFloat visualContentY = self.contentView.frame.origin.y - rawY;
155
+ NSString *value = [NSString stringWithFormat:@"node=%d rawY=%.2f treeY=%d maxY=%.2f minRawY=%.2f maxRawY=%.2f overTop=%.2f overBottom=%.2f visualY=%.2f scale=%.3f tracking=%d dragging=%d decel=%d",
156
+ self.nodeId, rawY, treeY, maxY, self.minObservedContentOffsetY, self.maxObservedContentOffsetY,
157
+ overTop, overBottom, visualContentY, scale, self.tracking ? 1 : 0, self.dragging ? 1 : 0,
158
+ self.decelerating ? 1 : 0];
159
+ self.accessibilityValue = value;
160
+
161
+ if (gea::ios::renderer::scrollDebugMarkersEnabled()) {
162
+ [self ensureDebugMarkers];
163
+ self.debugTelemetryLabel.text = [NSString stringWithFormat:@"rawY=%.1f treeY=%d maxY=%.1f\nmin=%.1f max=%.1f over=%.1f/%.1f",
164
+ rawY, treeY, maxY, self.minObservedContentOffsetY,
165
+ self.maxObservedContentOffsetY, overTop, overBottom];
166
+ self.debugTelemetryLabel.accessibilityValue = value;
167
+ self.debugBottomMarkerLabel.text = [NSString stringWithFormat:@" content y=max %.0f", maxY];
168
+ [self layoutDebugMarkers];
169
+ }
170
+ }
171
+
172
+ - (void)commitNativeScrollTopWithScale:(CGFloat)scale
173
+ {
174
+ if (self.nodeId < 0) return;
175
+ auto &tree = gea::embedded::ui::Tree::instance();
176
+ if (self.nodeId >= tree.nodeCount()) return;
177
+
178
+ const CGFloat maxOffset = std::max<CGFloat>(0, self.contentSize.height - self.bounds.size.height);
179
+ const CGFloat rawOffset = self.contentOffset.y;
180
+ if (rawOffset < -0.5 || rawOffset > maxOffset + 0.5) {
181
+ [self updateScrollTelemetryWithScale:scale];
182
+ return;
183
+ }
184
+
185
+ const CGFloat logicalOffset = std::clamp(rawOffset, static_cast<CGFloat>(0), maxOffset);
186
+ const int before = tree.scrollTop(self.nodeId);
187
+ const int next = static_cast<int>(std::lround(logicalOffset / std::max<CGFloat>(scale, 0.0001)));
188
+ tree.setScrollTop(self.nodeId, next);
189
+ if (tree.scrollTop(self.nodeId) != before && tree.mountedRoot() >= 0) {
190
+ tree.refresh(tree.mountedRoot(), tree.mountedWidth(), tree.mountedHeight());
191
+ }
192
+ [self updateScrollTelemetryWithScale:scale];
193
+ }
194
+
195
+ - (void)handleTap:(UITapGestureRecognizer *)recognizer
196
+ {
197
+ if (recognizer.state != UIGestureRecognizerStateEnded) return;
198
+ gea::ios::renderer::dispatchSyntheticTap(self.superview, [recognizer locationInView:self.superview]);
199
+ }
200
+
201
+ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
202
+ {
203
+ if (self.nodeId < 0) return;
204
+ (void)scrollView;
205
+ const CGFloat scale = gea::ios::renderer::canvasScaleForView(self.superview);
206
+ if (self.syncingFromTree) {
207
+ [self updateScrollTelemetryWithScale:scale];
208
+ return;
209
+ }
210
+ const CGFloat maxOffset = std::max<CGFloat>(0, scrollView.contentSize.height - scrollView.bounds.size.height);
211
+ const CGFloat rawOffset = scrollView.contentOffset.y;
212
+ const BOOL nativeScrollActive = scrollView.tracking || scrollView.dragging || scrollView.decelerating;
213
+ const BOOL rubberBanding = rawOffset < -0.5 || rawOffset > maxOffset + 0.5;
214
+ if (nativeScrollActive || rubberBanding) {
215
+ [self updateScrollTelemetryWithScale:scale];
216
+ return;
217
+ }
218
+ [self commitNativeScrollTopWithScale:scale];
219
+ }
220
+
221
+ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
222
+ {
223
+ (void)scrollView;
224
+ if (decelerate) return;
225
+ [self commitNativeScrollTopWithScale:gea::ios::renderer::canvasScaleForView(self.superview)];
226
+ }
227
+
228
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
229
+ {
230
+ (void)scrollView;
231
+ [self commitNativeScrollTopWithScale:gea::ios::renderer::canvasScaleForView(self.superview)];
232
+ }
233
+
234
+ - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
235
+ {
236
+ (void)scrollView;
237
+ [self commitNativeScrollTopWithScale:gea::ios::renderer::canvasScaleForView(self.superview)];
238
+ }
239
+
240
+ @end
241
+
242
+ namespace gea::ios::renderer {
243
+
244
+ void applyScrollProps(GeaNativeScrollContainer *scrollView,
245
+ const gea::embedded::ui::Node &node,
246
+ int nodeId,
247
+ CGFloat scale)
248
+ {
249
+ scrollView.nodeId = nodeId;
250
+ const CGFloat w = static_cast<CGFloat>(node.layout.width) * scale;
251
+ const CGFloat h = static_cast<CGFloat>(std::max<int>(node.layout.height, node.layout.scroll_content_height)) * scale;
252
+ const CGSize nextContentSize = CGSizeMake(w, h);
253
+ if (std::fabs(scrollView.contentSize.width - nextContentSize.width) > 0.5 ||
254
+ std::fabs(scrollView.contentSize.height - nextContentSize.height) > 0.5) {
255
+ scrollView.contentSize = nextContentSize;
256
+ }
257
+ const CGRect nextContentFrame = CGRectMake(0, 0, w, h);
258
+ if (!rectNearlyEqual(scrollView.contentView.frame, nextContentFrame)) scrollView.contentView.frame = nextContentFrame;
259
+ scrollView.hidden = node.style.display == 1 || node.style.opacity == 0;
260
+ scrollView.userInteractionEnabled = !scrollView.hidden;
261
+
262
+ const CGFloat maxOffset = std::max<CGFloat>(0, scrollView.contentSize.height - scrollView.bounds.size.height);
263
+ const CGFloat rawOffset = scrollView.contentOffset.y;
264
+ const BOOL nativeScrollActive = scrollView.tracking || scrollView.dragging || scrollView.decelerating;
265
+ const BOOL rubberBanding = rawOffset < -0.5 || rawOffset > maxOffset + 0.5;
266
+ if (!nativeScrollActive && !rubberBanding) {
267
+ const CGFloat desired = std::clamp(static_cast<CGFloat>(gea::embedded::ui::Tree::instance().scrollTop(nodeId)) * scale,
268
+ static_cast<CGFloat>(0),
269
+ maxOffset);
270
+ if (std::fabs(scrollView.contentOffset.y - desired) > 0.5) {
271
+ scrollView.syncingFromTree = YES;
272
+ scrollView.contentOffset = CGPointMake(0, desired);
273
+ scrollView.syncingFromTree = NO;
274
+ }
275
+ }
276
+ [scrollView updateScrollTelemetryWithScale:scale];
277
+ }
278
+
279
+ } // namespace gea::ios::renderer