@reactiive/ennio 0.0.1
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/EnnioCore.podspec +61 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/android/CMakeLists.txt +40 -0
- package/android/build.gradle +64 -0
- package/cpp/ElementMatcher.cpp +661 -0
- package/cpp/ElementMatcher.hpp +244 -0
- package/cpp/EnnioLog.hpp +182 -0
- package/cpp/HybridEnnio.cpp +1161 -0
- package/cpp/HybridEnnio.hpp +174 -0
- package/cpp/IdleMonitor.hpp +277 -0
- package/cpp/Protocol.cpp +135 -0
- package/cpp/Protocol.hpp +47 -0
- package/cpp/SelectorCriteria.hpp +281 -0
- package/cpp/SelectorParser.cpp +649 -0
- package/cpp/SelectorParser.hpp +94 -0
- package/cpp/ShadowTreeTraverser.cpp +305 -0
- package/cpp/ShadowTreeTraverser.hpp +142 -0
- package/cpp/TestIDRegistry.cpp +109 -0
- package/cpp/TestIDRegistry.hpp +84 -0
- package/dist/cli.js +16221 -0
- package/ios/EnnioAutoInit.mm +338 -0
- package/ios/EnnioDebugBanner.h +19 -0
- package/ios/EnnioDebugBanner.mm +178 -0
- package/ios/EnnioRuntimeHelper.h +264 -0
- package/ios/EnnioRuntimeHelper.mm +2443 -0
- package/lib/Ennio.nitro.d.ts +263 -0
- package/lib/Ennio.nitro.d.ts.map +1 -0
- package/lib/Ennio.nitro.js +2 -0
- package/lib/Ennio.nitro.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +45 -0
- package/lib/index.js.map +1 -0
- package/nitro.json +24 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.cmake +81 -0
- package/nitrogen/generated/android/EnnioCore+autolinking.gradle +27 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.cpp +49 -0
- package/nitrogen/generated/android/EnnioCoreOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ennio/EnnioCoreOnLoad.kt +35 -0
- package/nitrogen/generated/ios/EnnioCore+autolinking.rb +62 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/EnnioCore-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.mm +35 -0
- package/nitrogen/generated/ios/EnnioCoreAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/ExtendedElementInfo.hpp +118 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.cpp +44 -0
- package/nitrogen/generated/shared/c++/HybridEnnioSpec.hpp +93 -0
- package/nitrogen/generated/shared/c++/LayoutMetrics.hpp +103 -0
- package/nitrogen/generated/shared/c++/ScrollDirection.hpp +84 -0
- package/package.json +78 -0
- package/react-native.config.js +14 -0
- package/src/Ennio.nitro.ts +363 -0
- package/src/cli/hid-daemon.py +129 -0
- package/src/index.ts +72 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
//
|
|
2
|
+
// EnnioAutoInit.mm
|
|
3
|
+
// Automatic initialization of Ennio by hooking into React Native setup
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
#import <Foundation/Foundation.h>
|
|
7
|
+
#import <objc/runtime.h>
|
|
8
|
+
#import <objc/message.h>
|
|
9
|
+
#import "EnnioRuntimeHelper.h"
|
|
10
|
+
#import "EnnioDebugBanner.h"
|
|
11
|
+
|
|
12
|
+
#if __has_include(<React/RCTSurfacePresenter.h>)
|
|
13
|
+
#import <React/RCTSurfacePresenter.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
#include <jsi/jsi.h>
|
|
17
|
+
#include <functional>
|
|
18
|
+
#include "../cpp/HybridEnnio.hpp"
|
|
19
|
+
|
|
20
|
+
#if __has_include(<ReactCommon/RCTInstance.h>)
|
|
21
|
+
#import <ReactCommon/RCTInstance.h>
|
|
22
|
+
#define ENNIO_HAVE_RCTINSTANCE 1
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
// Flag to track if Ennio has been initialized
|
|
26
|
+
static BOOL _ennioInitialized = NO;
|
|
27
|
+
|
|
28
|
+
// Distribution channel detection. Ennio is a dev / QA tool and must
|
|
29
|
+
// refuse to start on App Store production or Enterprise builds. The
|
|
30
|
+
// pod is already excluded from Release configurations at the
|
|
31
|
+
// CocoaPods level, so this is a runtime backstop in case a custom
|
|
32
|
+
// build setup links Ennio into a production binary anyway.
|
|
33
|
+
typedef NS_ENUM(NSInteger, EnnioDistribution) {
|
|
34
|
+
EnnioDistDev,
|
|
35
|
+
EnnioDistAdHoc,
|
|
36
|
+
EnnioDistTestFlight,
|
|
37
|
+
EnnioDistAppStore,
|
|
38
|
+
EnnioDistEnterprise,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
static EnnioDistribution ennioDetectDistribution(void) {
|
|
42
|
+
#if TARGET_OS_SIMULATOR
|
|
43
|
+
// Simulator builds ship a placeholder StoreKit receipt named
|
|
44
|
+
// "receipt" (same name App Store production builds use), which the
|
|
45
|
+
// logic below would mis-classify as App Store and refuse to start.
|
|
46
|
+
// The Simulator runs no production binary by definition, so short-
|
|
47
|
+
// circuit to Dev before anything else.
|
|
48
|
+
return EnnioDistDev;
|
|
49
|
+
#else
|
|
50
|
+
NSURL* receiptURL = [NSBundle mainBundle].appStoreReceiptURL;
|
|
51
|
+
NSString* receiptName = receiptURL.lastPathComponent;
|
|
52
|
+
|
|
53
|
+
NSString* profilePath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
|
|
54
|
+
NSData* profileData = profilePath ? [NSData dataWithContentsOfFile:profilePath] : nil;
|
|
55
|
+
// .mobileprovision is a CMS-signed blob; the embedded plist XML is
|
|
56
|
+
// ASCII inside binary noise. Latin-1 makes containsString: work
|
|
57
|
+
// without decode failures on the surrounding bytes.
|
|
58
|
+
NSString* profileStr = profileData
|
|
59
|
+
? [[NSString alloc] initWithData:profileData encoding:NSISOLatin1StringEncoding]
|
|
60
|
+
: nil;
|
|
61
|
+
|
|
62
|
+
BOOL provisionsAllDevices = [profileStr containsString:@"<key>ProvisionsAllDevices</key>"];
|
|
63
|
+
BOOL hasProvisionedDevices = [profileStr containsString:@"<key>ProvisionedDevices</key>"];
|
|
64
|
+
BOOL hasProfile = (profileStr.length > 0);
|
|
65
|
+
|
|
66
|
+
// Enterprise: profile claims to provision all devices.
|
|
67
|
+
if (provisionsAllDevices) return EnnioDistEnterprise;
|
|
68
|
+
|
|
69
|
+
// Receipt-based channel detection (the device chose this at install
|
|
70
|
+
// time, not the build).
|
|
71
|
+
if ([receiptName isEqualToString:@"receipt"]) return EnnioDistAppStore;
|
|
72
|
+
if ([receiptName isEqualToString:@"sandboxReceipt"]) return EnnioDistTestFlight;
|
|
73
|
+
|
|
74
|
+
// Ad-Hoc: profile names a specific device list.
|
|
75
|
+
if (hasProvisionedDevices) return EnnioDistAdHoc;
|
|
76
|
+
|
|
77
|
+
// App Store distribution profile: no ProvisionedDevices, no
|
|
78
|
+
// ProvisionsAllDevices, and no receipt yet. This catches the
|
|
79
|
+
// sideloaded-production-IPA edge case where the build is signed
|
|
80
|
+
// with the App Store profile but Xcode/Configurator pushed it
|
|
81
|
+
// straight to a device without going through TestFlight or App
|
|
82
|
+
// Store, so no receipt was fetched.
|
|
83
|
+
if (hasProfile && !hasProvisionedDevices && !provisionsAllDevices) return EnnioDistAppStore;
|
|
84
|
+
|
|
85
|
+
// Xcode-attached debug builds on real hardware with no embedded
|
|
86
|
+
// profile at all. Treat as Dev.
|
|
87
|
+
return EnnioDistDev;
|
|
88
|
+
#endif
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static NSString* ennioDistributionName(EnnioDistribution d) {
|
|
92
|
+
switch (d) {
|
|
93
|
+
case EnnioDistDev: return @"Development";
|
|
94
|
+
case EnnioDistAdHoc: return @"Ad-Hoc";
|
|
95
|
+
case EnnioDistTestFlight: return @"TestFlight";
|
|
96
|
+
case EnnioDistAppStore: return @"App Store";
|
|
97
|
+
case EnnioDistEnterprise: return @"Enterprise";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hook into RCTHost's start method to capture the surface presenter
|
|
103
|
+
*/
|
|
104
|
+
@interface EnnioAutoInit : NSObject
|
|
105
|
+
@end
|
|
106
|
+
|
|
107
|
+
@implementation EnnioAutoInit
|
|
108
|
+
|
|
109
|
+
+ (void)load {
|
|
110
|
+
NSLog(@"[Ennio] EnnioAutoInit +load called");
|
|
111
|
+
// Diagnostic marker: when stdout/NSLog gets swallowed (idevicesyslog
|
|
112
|
+
// sometimes drops app debug lines on device), the file proves +load ran.
|
|
113
|
+
NSString* mark = [NSString stringWithFormat:@"%@/Library/_ennio_load_fired.txt", NSHomeDirectory()];
|
|
114
|
+
[@"loaded" writeToFile:mark atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
115
|
+
|
|
116
|
+
// Try to swizzle RCTHost's start method
|
|
117
|
+
Class hostClass = NSClassFromString(@"RCTHost");
|
|
118
|
+
if (!hostClass) {
|
|
119
|
+
NSLog(@"[Ennio] RCTHost class not found, trying alternative...");
|
|
120
|
+
|
|
121
|
+
// Try RCTFabricSurface's start method as alternative
|
|
122
|
+
Class surfaceClass = NSClassFromString(@"RCTFabricSurface");
|
|
123
|
+
if (surfaceClass) {
|
|
124
|
+
[self swizzleFabricSurfaceStart:surfaceClass];
|
|
125
|
+
} else {
|
|
126
|
+
NSLog(@"[Ennio] No suitable class found for auto-init");
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
NSLog(@"[Ennio] Found RCTHost class, setting up swizzling...");
|
|
132
|
+
|
|
133
|
+
SEL originalSelector = @selector(start);
|
|
134
|
+
SEL swizzledSelector = @selector(ennio_start);
|
|
135
|
+
|
|
136
|
+
Method originalMethod = class_getInstanceMethod(hostClass, originalSelector);
|
|
137
|
+
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
|
|
138
|
+
|
|
139
|
+
if (!originalMethod) {
|
|
140
|
+
NSLog(@"[Ennio] Could not find RCTHost start method");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add swizzled method to RCTHost class
|
|
145
|
+
BOOL didAddMethod = class_addMethod(
|
|
146
|
+
hostClass,
|
|
147
|
+
swizzledSelector,
|
|
148
|
+
method_getImplementation(swizzledMethod),
|
|
149
|
+
method_getTypeEncoding(swizzledMethod)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!didAddMethod) {
|
|
153
|
+
NSLog(@"[Ennio] Could not add swizzled method to RCTHost");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Exchange implementations
|
|
158
|
+
Method newSwizzledMethod = class_getInstanceMethod(hostClass, swizzledSelector);
|
|
159
|
+
method_exchangeImplementations(originalMethod, newSwizzledMethod);
|
|
160
|
+
NSLog(@"[Ennio] RCTHost.start swizzle installed successfully");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
+ (void)swizzleFabricSurfaceStart:(Class)surfaceClass {
|
|
164
|
+
SEL originalSelector = @selector(start);
|
|
165
|
+
SEL swizzledSelector = @selector(ennio_surfaceStart);
|
|
166
|
+
|
|
167
|
+
Method originalMethod = class_getInstanceMethod(surfaceClass, originalSelector);
|
|
168
|
+
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
|
|
169
|
+
|
|
170
|
+
if (!originalMethod) {
|
|
171
|
+
NSLog(@"[Ennio] Could not find RCTFabricSurface start method");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
BOOL didAddMethod = class_addMethod(
|
|
176
|
+
surfaceClass,
|
|
177
|
+
swizzledSelector,
|
|
178
|
+
method_getImplementation(swizzledMethod),
|
|
179
|
+
method_getTypeEncoding(swizzledMethod)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (didAddMethod) {
|
|
183
|
+
Method newSwizzledMethod = class_getInstanceMethod(surfaceClass, swizzledSelector);
|
|
184
|
+
method_exchangeImplementations(originalMethod, newSwizzledMethod);
|
|
185
|
+
NSLog(@"[Ennio] RCTFabricSurface.start swizzle installed successfully");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Swizzled RCTHost start method
|
|
190
|
+
- (void)ennio_start {
|
|
191
|
+
NSLog(@"[Ennio] RCTHost.start called");
|
|
192
|
+
|
|
193
|
+
// Call original implementation
|
|
194
|
+
[self ennio_start];
|
|
195
|
+
|
|
196
|
+
// Distribution gate. Even if the CocoaPods `:configurations`
|
|
197
|
+
// gate slipped and Ennio is linked into an App Store / Enterprise
|
|
198
|
+
// binary, refuse to install `__ennioDispatch`, the commit hook,
|
|
199
|
+
// or the ribbon. Runtime backstop on top of the build-time gate.
|
|
200
|
+
EnnioDistribution dist = ennioDetectDistribution();
|
|
201
|
+
if (dist == EnnioDistAppStore || dist == EnnioDistEnterprise) {
|
|
202
|
+
NSLog(@"[Ennio] REFUSING to start: %@ distribution detected. "
|
|
203
|
+
@"Ennio must never run in App Store or Enterprise builds. "
|
|
204
|
+
@"Your build pipeline is leaking a remote-control surface — fix it.",
|
|
205
|
+
ennioDistributionName(dist));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
NSLog(@"[Ennio] Distribution: %@ — starting", ennioDistributionName(dist));
|
|
209
|
+
|
|
210
|
+
// Pure-native bootstrap: pull RCTInstance from RCTHost's `_instance`
|
|
211
|
+
// ivar, ask it to run a block on the JS thread once the runtime is
|
|
212
|
+
// ready. Inside that block we have a `jsi::Runtime&` — capture it,
|
|
213
|
+
// install the commit-signal walker + the `__ennioDispatch` JSI host
|
|
214
|
+
// function. After this, the user's app never has to import `ennio`;
|
|
215
|
+
// the package autolinks via Pod and bootstraps entirely from native,
|
|
216
|
+
// and the CLI drives it over Hermes Inspector CDP.
|
|
217
|
+
Ivar instanceIvar = class_getInstanceVariable([self class], "_instance");
|
|
218
|
+
if (instanceIvar) {
|
|
219
|
+
id rctInstance = object_getIvar(self, instanceIvar);
|
|
220
|
+
#ifdef ENNIO_HAVE_RCTINSTANCE
|
|
221
|
+
if ([rctInstance isKindOfClass:[RCTInstance class]]) {
|
|
222
|
+
__strong RCTInstance* strongInstance = (RCTInstance*)rctInstance;
|
|
223
|
+
// Background dispatch workers need to schedule the
|
|
224
|
+
// response-write back onto the JS thread (jsi::Runtime is
|
|
225
|
+
// not thread-safe). RCTInstance.callFunctionOnBufferedRuntimeExecutor
|
|
226
|
+
// is the only sanctioned scheduler that hands us a
|
|
227
|
+
// Runtime& on the JS thread; wrap it as a std::function
|
|
228
|
+
// and stash it via HybridEnnio for the worker thread to
|
|
229
|
+
// pick up.
|
|
230
|
+
margelo::nitro::ennio::HybridEnnio::JSThreadExecutor exec =
|
|
231
|
+
[strongInstance](std::function<void(facebook::jsi::Runtime&)>&& fn) {
|
|
232
|
+
[strongInstance callFunctionOnBufferedRuntimeExecutor:std::move(fn)];
|
|
233
|
+
};
|
|
234
|
+
margelo::nitro::ennio::HybridEnnio::setJSThreadExecutor(std::move(exec));
|
|
235
|
+
|
|
236
|
+
// Bootstrap (capture runtime, install commit signal +
|
|
237
|
+
// `__ennioDispatch` JSI host function) once the JS thread
|
|
238
|
+
// is ready. RCTInstance delivers our C++ lambda onto the
|
|
239
|
+
// JS thread.
|
|
240
|
+
std::function<void(facebook::jsi::Runtime&)> boot =
|
|
241
|
+
[](facebook::jsi::Runtime& rt) {
|
|
242
|
+
NSString* m = [NSString stringWithFormat:@"%@/Library/_ennio_jsthread_fired.txt", NSHomeDirectory()];
|
|
243
|
+
[@"jsthread" writeToFile:m atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
244
|
+
margelo::nitro::ennio::HybridEnnio::nativeBootstrap(rt);
|
|
245
|
+
NSString* m2 = [NSString stringWithFormat:@"%@/Library/_ennio_bootstrap_returned.txt", NSHomeDirectory()];
|
|
246
|
+
[@"returned" writeToFile:m2 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
247
|
+
};
|
|
248
|
+
[strongInstance callFunctionOnBufferedRuntimeExecutor:std::move(boot)];
|
|
249
|
+
NSLog(@"[Ennio] Scheduled nativeBootstrap on JS thread");
|
|
250
|
+
// Diagnostic marker: confirms ennio_start ran AND nativeBootstrap was scheduled.
|
|
251
|
+
NSString* mark2 = [NSString stringWithFormat:@"%@/Library/_ennio_bootstrap_scheduled.txt", NSHomeDirectory()];
|
|
252
|
+
[[NSString stringWithFormat:@"dist=%@", ennioDistributionName(dist)]
|
|
253
|
+
writeToFile:mark2 atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
254
|
+
|
|
255
|
+
// Loud, greppable announce. If this line ever shows up in
|
|
256
|
+
// Console.app on a non-dev device, the build pipeline has
|
|
257
|
+
// leaked Ennio into a build it shouldn't be in.
|
|
258
|
+
NSLog(@"[Ennio] __ennioDispatch host function installed (distribution: %@) — "
|
|
259
|
+
@"if you see this in production, your build pipeline is broken.",
|
|
260
|
+
ennioDistributionName(dist));
|
|
261
|
+
|
|
262
|
+
// Show the top-right "E2E" ribbon — opt-in via the
|
|
263
|
+
// `ENNIORibbonEnabled` Info.plist key (written by
|
|
264
|
+
// `ennio-expo-plugin` when `showRibbon: true`). Default
|
|
265
|
+
// off so devs iterating on UI don't get an always-on
|
|
266
|
+
// overlay. Release builds don't compile this file at all
|
|
267
|
+
// (CocoaPods :configurations gate), so the check is only
|
|
268
|
+
// ever reached in Debug.
|
|
269
|
+
id ribbonFlag = [[NSBundle mainBundle].infoDictionary objectForKey:@"ENNIORibbonEnabled"];
|
|
270
|
+
BOOL showRibbon = [ribbonFlag isKindOfClass:[NSNumber class]] && [(NSNumber*)ribbonFlag boolValue];
|
|
271
|
+
if (showRibbon) {
|
|
272
|
+
[EnnioDebugBanner show];
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
NSLog(@"[Ennio] _instance is not an RCTInstance (got %@)", [rctInstance class]);
|
|
276
|
+
}
|
|
277
|
+
#else
|
|
278
|
+
NSLog(@"[Ennio] RCTInstance.h not available — falling back to JS-side bootstrap");
|
|
279
|
+
(void)rctInstance;
|
|
280
|
+
#endif
|
|
281
|
+
} else {
|
|
282
|
+
NSLog(@"[Ennio] Could not find RCTHost._instance ivar");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get surface presenter from RCTHost
|
|
286
|
+
if (!_ennioInitialized) {
|
|
287
|
+
// Use performSelector to avoid compile-time dependency
|
|
288
|
+
SEL surfacePresenterSel = @selector(surfacePresenter);
|
|
289
|
+
if ([self respondsToSelector:surfacePresenterSel]) {
|
|
290
|
+
#pragma clang diagnostic push
|
|
291
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
292
|
+
id surfacePresenter = [self performSelector:surfacePresenterSel];
|
|
293
|
+
#pragma clang diagnostic pop
|
|
294
|
+
|
|
295
|
+
if (surfacePresenter) {
|
|
296
|
+
EnnioSetSurfacePresenter((__bridge RCTSurfacePresenter *)(__bridge void *)surfacePresenter);
|
|
297
|
+
_ennioInitialized = YES;
|
|
298
|
+
NSLog(@"[Ennio] Surface presenter captured from RCTHost");
|
|
299
|
+
} else {
|
|
300
|
+
NSLog(@"[Ennio] RCTHost.surfacePresenter returned nil");
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
NSLog(@"[Ennio] RCTHost does not respond to surfacePresenter");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Swizzled RCTFabricSurface start method (alternative path)
|
|
309
|
+
- (void)ennio_surfaceStart {
|
|
310
|
+
NSLog(@"[Ennio] RCTFabricSurface.start called");
|
|
311
|
+
|
|
312
|
+
// Call original implementation
|
|
313
|
+
[self ennio_surfaceStart];
|
|
314
|
+
|
|
315
|
+
// Try to get surface presenter from the surface
|
|
316
|
+
if (!_ennioInitialized) {
|
|
317
|
+
// The surface should have access to its presenter
|
|
318
|
+
// This is a fallback - not all surfaces expose this
|
|
319
|
+
NSLog(@"[Ennio] Attempting to find surface presenter from RCTFabricSurface...");
|
|
320
|
+
|
|
321
|
+
// Check if there's a presenter property
|
|
322
|
+
SEL presenterSel = @selector(surfacePresenter);
|
|
323
|
+
if ([self respondsToSelector:presenterSel]) {
|
|
324
|
+
#pragma clang diagnostic push
|
|
325
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
326
|
+
id presenter = [self performSelector:presenterSel];
|
|
327
|
+
#pragma clang diagnostic pop
|
|
328
|
+
|
|
329
|
+
if (presenter) {
|
|
330
|
+
EnnioSetSurfacePresenter((__bridge RCTSurfacePresenter *)(__bridge void *)presenter);
|
|
331
|
+
_ennioInitialized = YES;
|
|
332
|
+
NSLog(@"[Ennio] Surface presenter captured from RCTFabricSurface");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
@end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// EnnioDebugBanner.h
|
|
3
|
+
// Flutter-style top-right diagonal "E2E" ribbon, shown whenever Ennio
|
|
4
|
+
// is active in the running build. Touches pass through — the ribbon
|
|
5
|
+
// never intercepts user interaction or test-runner taps.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <Foundation/Foundation.h>
|
|
9
|
+
|
|
10
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
11
|
+
|
|
12
|
+
@interface EnnioDebugBanner : NSObject
|
|
13
|
+
|
|
14
|
+
/// Idempotent. Safe to call from any thread; attach happens on main.
|
|
15
|
+
+ (void)show;
|
|
16
|
+
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//
|
|
2
|
+
// EnnioDebugBanner.mm
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "EnnioDebugBanner.h"
|
|
6
|
+
#import <UIKit/UIKit.h>
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Pass-through window: hit-test always nil, so taps never land on the
|
|
10
|
+
// banner — they fall through to whatever app view is below. Same trick
|
|
11
|
+
// Flutter uses for its DEBUG ribbon.
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
@interface EnnioPassthroughWindow : UIWindow
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
@implementation EnnioPassthroughWindow
|
|
18
|
+
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { return nil; }
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Ribbon view: 150x150 container anchored top-right. A red UIView
|
|
23
|
+
// "stripe" subview sits diagonally across the corner via a +45°
|
|
24
|
+
// rotation transform — same orientation as Flutter's debug banner.
|
|
25
|
+
// A bold white UILabel inside the stripe reads naturally along the
|
|
26
|
+
// diagonal. No custom drawRect → CALayer handles all the geometry,
|
|
27
|
+
// shadow, and text rasterisation cleanly.
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
static CGFloat const kEnnioBannerSize = 130.0;
|
|
31
|
+
static CGFloat const kEnnioStripeWidth = 160.0; // longer than diagonal
|
|
32
|
+
static CGFloat const kEnnioStripeHeight = 18.0;
|
|
33
|
+
static CGFloat const kEnnioStripeFromCorner = 42.0; // px from corner (along diagonal)
|
|
34
|
+
|
|
35
|
+
@interface EnnioRibbonView : UIView
|
|
36
|
+
@end
|
|
37
|
+
|
|
38
|
+
@implementation EnnioRibbonView
|
|
39
|
+
|
|
40
|
+
- (instancetype)init {
|
|
41
|
+
self = [super initWithFrame:CGRectMake(0, 0, kEnnioBannerSize, kEnnioBannerSize)];
|
|
42
|
+
if (self) {
|
|
43
|
+
self.backgroundColor = [UIColor clearColor];
|
|
44
|
+
self.userInteractionEnabled = NO;
|
|
45
|
+
self.clipsToBounds = NO;
|
|
46
|
+
|
|
47
|
+
// Stripe — solid red, with a soft shadow so it stays visible
|
|
48
|
+
// on any background.
|
|
49
|
+
UIView* stripe = [[UIView alloc] initWithFrame:
|
|
50
|
+
CGRectMake(0, 0, kEnnioStripeWidth, kEnnioStripeHeight)];
|
|
51
|
+
stripe.backgroundColor = [UIColor systemRedColor];
|
|
52
|
+
stripe.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
53
|
+
stripe.layer.shadowOpacity = 0.30;
|
|
54
|
+
stripe.layer.shadowOffset = CGSizeMake(0, 1);
|
|
55
|
+
stripe.layer.shadowRadius = 2.5;
|
|
56
|
+
stripe.userInteractionEnabled = NO;
|
|
57
|
+
|
|
58
|
+
// Label — bold white "E2E", filling the stripe bounds, drawn
|
|
59
|
+
// in unrotated coordinates so the rotation transform applies
|
|
60
|
+
// to label + background as one unit.
|
|
61
|
+
UILabel* label = [[UILabel alloc] initWithFrame:stripe.bounds];
|
|
62
|
+
label.text = @"E2E";
|
|
63
|
+
label.font = [UIFont systemFontOfSize:10 weight:UIFontWeightHeavy];
|
|
64
|
+
label.textColor = [UIColor whiteColor];
|
|
65
|
+
label.textAlignment = NSTextAlignmentCenter;
|
|
66
|
+
label.adjustsFontSizeToFitWidth = NO;
|
|
67
|
+
label.userInteractionEnabled = NO;
|
|
68
|
+
// 1.5pt letter-spacing for slightly more breathing room.
|
|
69
|
+
NSMutableAttributedString* attr =
|
|
70
|
+
[[NSMutableAttributedString alloc] initWithString:label.text];
|
|
71
|
+
[attr addAttribute:NSKernAttributeName value:@1.0
|
|
72
|
+
range:NSMakeRange(0, label.text.length)];
|
|
73
|
+
[attr addAttribute:NSFontAttributeName value:label.font
|
|
74
|
+
range:NSMakeRange(0, label.text.length)];
|
|
75
|
+
[attr addAttribute:NSForegroundColorAttributeName value:label.textColor
|
|
76
|
+
range:NSMakeRange(0, label.text.length)];
|
|
77
|
+
label.attributedText = attr;
|
|
78
|
+
[stripe addSubview:label];
|
|
79
|
+
|
|
80
|
+
[self addSubview:stripe];
|
|
81
|
+
|
|
82
|
+
// Position stripe so its centre sits on the diagonal that runs
|
|
83
|
+
// from top-right corner toward the lower-left of the view.
|
|
84
|
+
// Distance `kEnnioStripeFromCorner` along that diagonal from
|
|
85
|
+
// the corner: (corner.x - d/√2, corner.y + d/√2).
|
|
86
|
+
const CGFloat half = kEnnioStripeFromCorner * M_SQRT1_2;
|
|
87
|
+
stripe.center = CGPointMake(kEnnioBannerSize - half, half);
|
|
88
|
+
|
|
89
|
+
// +45° rotation = clockwise from the stripe's natural
|
|
90
|
+
// horizontal orientation. With the stripe's centre on the
|
|
91
|
+
// top-right diagonal, the rotated stripe lies along that
|
|
92
|
+
// diagonal and "E2E" reads naturally with a 45° head tilt to
|
|
93
|
+
// the right — matches Flutter's topEnd debug banner.
|
|
94
|
+
stripe.transform = CGAffineTransformMakeRotation(M_PI_4);
|
|
95
|
+
}
|
|
96
|
+
return self;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@end
|
|
100
|
+
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
102
|
+
// Public banner singleton.
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
@interface EnnioDebugBanner ()
|
|
106
|
+
@property (nonatomic, strong, nullable) UIWindow* window;
|
|
107
|
+
@end
|
|
108
|
+
|
|
109
|
+
@implementation EnnioDebugBanner
|
|
110
|
+
|
|
111
|
+
+ (instancetype)shared {
|
|
112
|
+
static EnnioDebugBanner* instance;
|
|
113
|
+
static dispatch_once_t once;
|
|
114
|
+
dispatch_once(&once, ^{
|
|
115
|
+
instance = [[EnnioDebugBanner alloc] init];
|
|
116
|
+
});
|
|
117
|
+
return instance;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
+ (void)show {
|
|
121
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
122
|
+
[[self shared] attach];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
- (void)attach {
|
|
127
|
+
if (self.window) return;
|
|
128
|
+
|
|
129
|
+
UIWindowScene* scene = [self foregroundScene];
|
|
130
|
+
if (!scene) {
|
|
131
|
+
// Scene not ready yet — retry shortly. RCTHost.start fires
|
|
132
|
+
// before the first scene activates on cold launch.
|
|
133
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 250 * NSEC_PER_MSEC),
|
|
134
|
+
dispatch_get_main_queue(),
|
|
135
|
+
^{ [self attach]; });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
UIWindow* w = [[EnnioPassthroughWindow alloc] initWithWindowScene:scene];
|
|
140
|
+
w.windowLevel = UIWindowLevelStatusBar + 1;
|
|
141
|
+
w.backgroundColor = [UIColor clearColor];
|
|
142
|
+
w.userInteractionEnabled = NO;
|
|
143
|
+
|
|
144
|
+
UIViewController* vc = [[UIViewController alloc] init];
|
|
145
|
+
vc.view.backgroundColor = [UIColor clearColor];
|
|
146
|
+
vc.view.userInteractionEnabled = NO;
|
|
147
|
+
|
|
148
|
+
EnnioRibbonView* ribbon = [[EnnioRibbonView alloc] init];
|
|
149
|
+
ribbon.translatesAutoresizingMaskIntoConstraints = NO;
|
|
150
|
+
[vc.view addSubview:ribbon];
|
|
151
|
+
|
|
152
|
+
// Pin to top-right corner of the window — outside the safe area on
|
|
153
|
+
// purpose, matching Flutter's DEBUG banner that runs into the
|
|
154
|
+
// status bar / notch area.
|
|
155
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
156
|
+
[ribbon.topAnchor constraintEqualToAnchor:vc.view.topAnchor],
|
|
157
|
+
[ribbon.trailingAnchor constraintEqualToAnchor:vc.view.trailingAnchor],
|
|
158
|
+
[ribbon.widthAnchor constraintEqualToConstant:kEnnioBannerSize],
|
|
159
|
+
[ribbon.heightAnchor constraintEqualToConstant:kEnnioBannerSize],
|
|
160
|
+
]];
|
|
161
|
+
|
|
162
|
+
w.rootViewController = vc;
|
|
163
|
+
w.hidden = NO;
|
|
164
|
+
self.window = w;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
- (UIWindowScene*)foregroundScene {
|
|
168
|
+
for (UIScene* s in [UIApplication sharedApplication].connectedScenes) {
|
|
169
|
+
if (![s isKindOfClass:[UIWindowScene class]]) continue;
|
|
170
|
+
if (s.activationState == UISceneActivationStateForegroundActive ||
|
|
171
|
+
s.activationState == UISceneActivationStateForegroundInactive) {
|
|
172
|
+
return (UIWindowScene*)s;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return nil;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@end
|