@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,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
|