@symbiote-native/engine 0.5.0 → 1.0.0
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/README.md +39 -14
- package/android/CMakeLists.txt +51 -0
- package/android/build.gradle +90 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
- package/build/accessibility-info/shared.js +1 -1
- package/build/accessibility-props.d.ts +1 -8
- package/build/accessibility-props.js +13 -16
- package/build/animated/animations/composition.d.ts +1 -1
- package/build/animated/animations/composition.js +18 -4
- package/build/animated/easing.d.ts +3 -2
- package/build/animated/easing.js +17 -88
- package/build/animated/event.js +6 -1
- package/build/animated/host-binding.d.ts +1 -1
- package/build/animated/host-binding.js +19 -4
- package/build/animated/index.d.ts +1 -1
- package/build/animated/mock.d.ts +1 -19
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +16 -50
- package/build/events/index.js +88 -40
- package/build/fabric-props.d.ts +1 -1
- package/build/fabric-props.js +116 -184
- package/build/fabric.d.ts +9 -0
- package/build/fabric.js +32 -0
- package/build/host-access.d.ts +125 -0
- package/build/host-access.js +280 -0
- package/build/host-behavior.d.ts +84 -21
- package/build/host-behavior.js +196 -30
- package/build/image-source-write.d.ts +16 -0
- package/build/image-source-write.js +65 -0
- package/build/imperative.d.ts +49 -0
- package/build/imperative.js +258 -0
- package/build/index.d.ts +14 -7
- package/build/index.js +53 -10
- package/build/mutation-buffer.d.ts +222 -0
- package/build/mutation-buffer.js +491 -0
- package/build/native-engine.d.ts +182 -0
- package/build/native-engine.js +178 -0
- package/build/native-tree-host.d.ts +25 -0
- package/build/native-tree-host.js +66 -0
- package/build/node.d.ts +172 -57
- package/build/node.js +839 -383
- package/build/pan-responder/index.js +27 -52
- package/build/platform-color/index.d.ts +1 -1
- package/build/platform-color/index.js +11 -4
- package/build/process-background-image/index.js +30 -566
- package/build/process-background-longhands.d.ts +4 -0
- package/build/process-background-longhands.js +44 -0
- package/build/process-box-shadow/index.js +23 -187
- package/build/process-filter.js +27 -300
- package/build/process-transform/index.d.ts +1 -1
- package/build/process-transform/index.js +25 -107
- package/build/process-transform-origin/index.d.ts +1 -1
- package/build/process-transform-origin/index.js +29 -102
- package/build/registry.d.ts +36 -0
- package/build/registry.js +73 -0
- package/build/sound-manager/index.d.ts +3 -0
- package/build/sound-manager/index.js +36 -0
- package/build/structured-style.d.ts +10 -0
- package/build/structured-style.js +180 -0
- package/build/style-registry/index.d.ts +14 -0
- package/build/style-registry/index.js +60 -11
- package/build/surface.d.ts +31 -2
- package/build/surface.js +138 -56
- package/build/text-input-state.d.ts +1 -0
- package/build/text-input-state.js +17 -3
- package/build/tree-host.d.ts +307 -0
- package/build/tree-host.js +211 -0
- package/build/view-config.js +4 -4
- package/codegen-specs/NativeSymbioteEngine.ts +27 -0
- package/cpp/SymbioteDebug.cpp +51 -0
- package/cpp/SymbioteDebug.h +54 -0
- package/cpp/SymbioteEngineBindings.cpp +232 -0
- package/cpp/SymbioteEngineBindings.h +59 -0
- package/cpp/SymbioteFabricProps.cpp +2619 -0
- package/cpp/SymbioteFabricProps.h +223 -0
- package/cpp/SymbioteTree.cpp +2478 -0
- package/cpp/SymbioteTree.h +257 -0
- package/ios/SymbioteEngineModule.h +25 -0
- package/ios/SymbioteEngineModule.mm +44 -0
- package/package.json +31 -3
- package/react-native.config.cjs +23 -0
- package/symbiote-engine.podspec +42 -0
- package/build/animated/bezier.d.ts +0 -1
- package/build/animated/bezier.js +0 -102
- package/build/commit.d.ts +0 -49
- package/build/commit.js +0 -1058
- package/build/tags.d.ts +0 -2
- package/build/tags.js +0 -40
package/README.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# @symbiote-native/engine
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
The package at the bottom of [SymbioteNative](../../README.md) that every framework adapter
|
|
4
|
+
(`@symbiote-native/react`, `@symbiote-native/vue`, `@symbiote-native/angular`, …) drives, and the
|
|
5
|
+
only place the mutation→clone-on-write translation into React Native's Fabric exists. **The
|
|
6
|
+
retained tree itself lives in C++ now**, in `cpp/SymbioteTree.cpp`, reached through this package's
|
|
7
|
+
`src/` — a node handle returned to JS is a thin reference into that native tree, not a JS object
|
|
8
|
+
holding the tree's state.
|
|
9
|
+
|
|
10
|
+
An adapter still mutates a node cheaply (`appendChild` / `setProp` / `removeChild` …), but each
|
|
11
|
+
call now appends an op to the current commit's buffer instead of touching a tree directly. On
|
|
12
|
+
commit, the whole buffer crosses into C++ in **one** JSI call; `SymbioteTree` applies the ops
|
|
13
|
+
against the retained tree, clones only what changed, resolves the platform-parity rules keyed on
|
|
14
|
+
each node's Fabric tag (`cpp/SymbioteFabricProps.cpp` — RN's own component-level behavior, like
|
|
15
|
+
`Pressable`'s `disabled` fold or a `<Text>`'s default `ellipsizeMode`, ported once instead of per
|
|
16
|
+
adapter), and calls into Fabric's C++ `UIManager` directly to commit — the persistent,
|
|
17
|
+
clone-on-write dance Fabric requires, done **once**, for every framework, with one crossing per
|
|
18
|
+
commit instead of one per mutation.
|
|
10
19
|
|
|
11
20
|
> New to SymbioteNative? The [root README](../../README.md) has the architecture and the one fact it
|
|
12
21
|
> rests on — React is just _one client_ of `nativeFabricUIManager`. This package is what sits
|
|
@@ -101,10 +110,10 @@ before a tag is guaranteed to exist.
|
|
|
101
110
|
`ActionSheetIOS`, `Linking`, `Vibration`, `ToastAndroid`, `Settings`, `I18nManager`.
|
|
102
111
|
- **Host behaviors** (`registerHostBehavior` / `IHostBehavior` / `hasHostBehaviors` /
|
|
103
112
|
`clearHostBehaviors` / `appListenerFor` / `setBehaviorListener` / `requestCommitFor`) — the
|
|
104
|
-
registry that lets a primitive's state machine live directly on the engine node instead of
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
registry that lets a primitive's state machine live directly on the engine node instead of inside
|
|
114
|
+
a framework component, so `pressable`/`switch`/`text-input`/`image` are bare intrinsic tags.
|
|
115
|
+
`@symbiote-native/components` registers its behaviors against this seam; the engine never imports
|
|
116
|
+
them back.
|
|
108
117
|
- **`Animated`** — both the JS and native driver (`timing` / `spring` / `decay` / `loop` /
|
|
109
118
|
`ValueXY` / tracking / `diffClamp` / `Easing`), including the native-event attachment path
|
|
110
119
|
(`attachNativeEvent`, `AnimatedEvent`).
|
|
@@ -123,9 +132,11 @@ before a tag is guaranteed to exist.
|
|
|
123
132
|
|
|
124
133
|
- It does not know about React, Vue, Angular, JSX, templates, or reactivity — an adapter maps its
|
|
125
134
|
own framework idioms onto this API, never the other way around.
|
|
126
|
-
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
- The JS side (`src/`) does not touch Fabric, JSI, or Yoga directly — it only builds the
|
|
136
|
+
command buffer and hands it to `SymbioteTree` in one crossing. That C++ tree is the layer
|
|
137
|
+
that actually calls Fabric's `UIManager` (`createNode` / `cloneNodeWithNewProps` /
|
|
138
|
+
`appendChildToSet` / `completeRoot`), the same framework-agnostic seam React's own renderer
|
|
139
|
+
uses further up, from JS, through `nativeFabricUIManager`.
|
|
129
140
|
- It is not a component library — visual components (Switch, Modal, the lists, …) live in
|
|
130
141
|
[`@symbiote-native/components`](../components), built on top of this package's `Descriptor`-free
|
|
131
142
|
mutation API.
|
|
@@ -145,3 +156,17 @@ before a tag is guaranteed to exist.
|
|
|
145
156
|
pnpm test # vitest, from the workspace root — headless, against a fake Fabric slot
|
|
146
157
|
DEBUG=1 pnpm test # same, with diagnostic logs on
|
|
147
158
|
```
|
|
159
|
+
|
|
160
|
+
That covers the JS side (`src/`) against a fake `nativeFabricUIManager`. `SymbioteTree` itself
|
|
161
|
+
(`cpp/`) has its own test tiers, run from the workspace root:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pnpm test:cpp # gtest unit tests over SymbioteTree, asserts on — the build every PR runs
|
|
165
|
+
pnpm test:itest # the JS-driven correctness suite (core/engine/cpp/tests/js/*.itest.ts*),
|
|
166
|
+
# a real six-adapter integration harness against the C++ tree, asserts on
|
|
167
|
+
pnpm bench:itest # the same itest harness, Release build (NDEBUG + -O) — for timings, never asserts
|
|
168
|
+
pnpm test:android # test:itest built with -DSYMBIOTE_PLATFORM_ANDROID=ON, for the #ifdef ANDROID rules
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Never read `test:itest`'s timings — that build has asserts on and is measurably slower than what
|
|
172
|
+
ships; use `bench:itest` for anything you intend to quote a millisecond from.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# The Android half of the engine's native module. It compiles `../cpp` — the SAME sources the
|
|
2
|
+
# podspec globs — plus the JNI registration shim beside this file. Nothing under `cpp/` is copied
|
|
3
|
+
# or forked; the whole point of the split is that only the registration is per-platform.
|
|
4
|
+
#
|
|
5
|
+
# Shaped after the one third-party precedent that is already in this repo's own app trees and
|
|
6
|
+
# already builds against RN 0.86: `react-native-screens/android/CMakeLists.txt`.
|
|
7
|
+
|
|
8
|
+
cmake_minimum_required(VERSION 3.13)
|
|
9
|
+
|
|
10
|
+
project(symbiote_engine)
|
|
11
|
+
|
|
12
|
+
set(CMAKE_VERBOSE_MAKEFILE ON)
|
|
13
|
+
|
|
14
|
+
# Globbed, not listed, and that is deliberate: the iOS podspec globs `cpp/**/*.{h,cpp}`, so a listed
|
|
15
|
+
# set here would mean a new shared source compiles on one platform and not the other — a link error
|
|
16
|
+
# on Android for a symbol iOS resolves fine, arriving whenever somebody adds a file and forgets this
|
|
17
|
+
# line. RN's own fantom tester globs its sources for the same reason.
|
|
18
|
+
file(GLOB SYMBIOTE_SHARED_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../cpp/*.cpp)
|
|
19
|
+
|
|
20
|
+
add_library(symbiote_engine
|
|
21
|
+
SHARED
|
|
22
|
+
${SYMBIOTE_SHARED_SOURCES}
|
|
23
|
+
src/main/cpp/SymbioteEngineJni.cpp
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# So the JNI shim can say `#include "SymbioteEngineBindings.h"` rather than walk four levels up.
|
|
27
|
+
target_include_directories(symbiote_engine PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../cpp)
|
|
28
|
+
|
|
29
|
+
set_target_properties(symbiote_engine PROPERTIES
|
|
30
|
+
CXX_STANDARD 20
|
|
31
|
+
CXX_STANDARD_REQUIRED ON
|
|
32
|
+
CXX_EXTENSIONS OFF
|
|
33
|
+
POSITION_INDEPENDENT_CODE ON
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
target_compile_definitions(symbiote_engine PRIVATE -DFOLLY_NO_CONFIG=1)
|
|
37
|
+
|
|
38
|
+
# `jsi` and `reactnative` are the only two prefab modules ReactAndroid publishes
|
|
39
|
+
# (ReactAndroid/build.gradle.kts, the `prefab { }` block — the third is `hermestooling`).
|
|
40
|
+
# `reactnative` is the merged library that carries react/renderer/uimanager, react/renderer/mounting
|
|
41
|
+
# and the turbomodule JNI headers our C++ includes; there is no separate
|
|
42
|
+
# `react_renderer_uimanager` prefab target to link.
|
|
43
|
+
find_package(ReactAndroid REQUIRED CONFIG)
|
|
44
|
+
find_package(fbjni REQUIRED CONFIG)
|
|
45
|
+
|
|
46
|
+
target_link_libraries(symbiote_engine
|
|
47
|
+
ReactAndroid::reactnative
|
|
48
|
+
ReactAndroid::jsi
|
|
49
|
+
fbjni::fbjni
|
|
50
|
+
android
|
|
51
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @symbiote-native/engine's Android native half — the twin of `symbiote-engine.podspec`.
|
|
2
|
+
//
|
|
3
|
+
// `@symbiote-native/engine` is a DIRECT dependency of every example app, so RN's autolinking finds
|
|
4
|
+
// this project with no entry in anybody's `settings.gradle` — the same reason the podspec needs no
|
|
5
|
+
// Podfile line. Built as a subproject of the consuming app, so it reads the app's pinned SDK/NDK
|
|
6
|
+
// versions from `rootProject.ext` (see `examples/*/android/build.gradle`), falling back to the same
|
|
7
|
+
// defaults `packages/android` uses when built in isolation.
|
|
8
|
+
|
|
9
|
+
apply plugin: "com.android.library"
|
|
10
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
11
|
+
// Runs codegen over `../codegen-specs`, producing `NativeSymbioteEngineSpec` (Java) and the JNI spec
|
|
12
|
+
// the app's autolinking links. Needed for the module to be a real TURBO module: the legacy-interop
|
|
13
|
+
// path (`TurboModuleManager::getLegacyModule`) never consults `TurboModuleWithJSIBindings`, so a
|
|
14
|
+
// plain `ReactContextBaseJavaModule` — the shape `packages/android` uses — would install nothing.
|
|
15
|
+
// The plugin supplies `root`/`reactNativeDir`/`jsRootDir` for a library itself and adds the generated
|
|
16
|
+
// java dir to `sourceSets.main` (ReactPlugin.kt, `configureCodegen(isLibrary = true)`), so no
|
|
17
|
+
// `react { }` block and no `sourceSets` line are needed here.
|
|
18
|
+
apply plugin: "com.facebook.react"
|
|
19
|
+
|
|
20
|
+
def safeExtGet(prop, fallback) {
|
|
21
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
android {
|
|
25
|
+
namespace "dev.symbiotenative.engine"
|
|
26
|
+
compileSdk safeExtGet("compileSdkVersion", 36)
|
|
27
|
+
|
|
28
|
+
if (rootProject.hasProperty("ndkPath")) {
|
|
29
|
+
ndkPath rootProject.ext.ndkPath
|
|
30
|
+
}
|
|
31
|
+
if (rootProject.hasProperty("ndkVersion")) {
|
|
32
|
+
ndkVersion rootProject.ext.ndkVersion
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
defaultConfig {
|
|
36
|
+
minSdk safeExtGet("minSdkVersion", 24)
|
|
37
|
+
|
|
38
|
+
externalNativeBuild {
|
|
39
|
+
cmake {
|
|
40
|
+
arguments "-DANDROID_STL=c++_shared"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildFeatures {
|
|
46
|
+
// Consumes ReactAndroid's published `jsi` / `reactnative` prefab modules.
|
|
47
|
+
prefab true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
externalNativeBuild {
|
|
51
|
+
cmake {
|
|
52
|
+
path "CMakeLists.txt"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
compileOptions {
|
|
57
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
58
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
kotlinOptions {
|
|
62
|
+
jvmTarget = "17"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
packagingOptions {
|
|
66
|
+
// RN's own .so files get copied into this library's intermediates by prefab and would collide
|
|
67
|
+
// when the app merges native libs. Only `libsymbiote_engine.so` should ship from here. Since
|
|
68
|
+
// RN 0.76 the renderer/jni libraries are merged into `libreactnative.so`, so that one name
|
|
69
|
+
// covers all of them (same list as `react-native-screens`).
|
|
70
|
+
excludes = [
|
|
71
|
+
"META-INF",
|
|
72
|
+
"META-INF/**",
|
|
73
|
+
"**/libjsi.so",
|
|
74
|
+
"**/libc++_shared.so",
|
|
75
|
+
"**/libfbjni.so",
|
|
76
|
+
"**/libreactnative.so",
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
repositories {
|
|
82
|
+
google()
|
|
83
|
+
mavenCentral()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
dependencies {
|
|
87
|
+
// Version-less: the consuming app's React Native Gradle plugin pins react-android, and it brings
|
|
88
|
+
// fbjni's prefab in transitively (which is what `find_package(fbjni)` resolves).
|
|
89
|
+
implementation "com.facebook.react:react-android"
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Android's registration shim — the twin of `core/engine/ios/SymbioteEngineModule.mm`, and the only
|
|
2
|
+
// file in this folder that is not build configuration. Everything it does is hand the platform-shared
|
|
3
|
+
// `symbiote::installBindings` to whatever runtime React Native offers it.
|
|
4
|
+
//
|
|
5
|
+
// The seam is `TurboModuleWithJSIBindings`
|
|
6
|
+
// (ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/interfaces/TurboModuleWithJSIBindings.kt).
|
|
7
|
+
// `TurboModuleManager::getTurboModule` checks `isInstanceOf(JTurboModuleWithJSIBindings)` right after
|
|
8
|
+
// it creates the Java module and immediately calls `installer->cthis()->installBindings(runtime, ...)`
|
|
9
|
+
// (ReactAndroid/src/main/jni/react/turbomodule/ReactCommon/TurboModuleManager.cpp:205-216). That is
|
|
10
|
+
// exactly what `installJSIBindingsWithRuntime:callInvoker:` is on iOS: a hook nobody can skip who
|
|
11
|
+
// reaches the module at all, so JS resolves the module for its SIDE EFFECT and then reads the global.
|
|
12
|
+
//
|
|
13
|
+
// RN's own sample of this shape is
|
|
14
|
+
// ReactCommon/react/nativemodule/samples/platform/android/{SampleTurboModule.kt,ReactCommon/SampleTurboModuleJSIBindings.cpp}
|
|
15
|
+
// — a `jni::JavaClass` whose descriptor names the Kotlin module, `registerNatives` binding
|
|
16
|
+
// `getBindingsInstaller`, and a `BindingsInstallerHolder::newObjectCxxArgs` lambda. This file is that,
|
|
17
|
+
// with our installer inside the lambda.
|
|
18
|
+
|
|
19
|
+
#include <ReactCommon/BindingsInstallerHolder.h>
|
|
20
|
+
#include <ReactCommon/CallInvoker.h>
|
|
21
|
+
#include <fbjni/fbjni.h>
|
|
22
|
+
#include <jsi/jsi.h>
|
|
23
|
+
|
|
24
|
+
#include "SymbioteEngineBindings.h"
|
|
25
|
+
|
|
26
|
+
namespace symbiote {
|
|
27
|
+
|
|
28
|
+
using namespace facebook;
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The C++ side of `dev.symbiotenative.engine.SymbioteEngineModule`.
|
|
34
|
+
*
|
|
35
|
+
* Both of the Kotlin class's `external` members are bound here. `getVersion` is native rather than a
|
|
36
|
+
* Kotlin constant on purpose: `kNativeVersion` is the ABI gate `native-engine.ts` reads, and a second
|
|
37
|
+
* spelling of it in Kotlin is the kind of pair that drifts without anything going red.
|
|
38
|
+
*/
|
|
39
|
+
class SymbioteEngineJni : public jni::JavaClass<SymbioteEngineJni> {
|
|
40
|
+
public:
|
|
41
|
+
static constexpr const char *kJavaDescriptor = "Ldev/symbiotenative/engine/SymbioteEngineModule;";
|
|
42
|
+
|
|
43
|
+
static void registerNatives() {
|
|
44
|
+
javaClassLocal()->registerNatives({
|
|
45
|
+
makeNativeMethod("getBindingsInstaller", SymbioteEngineJni::getBindingsInstaller),
|
|
46
|
+
makeNativeMethod("getVersion", SymbioteEngineJni::getVersion),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private:
|
|
51
|
+
static jni::local_ref<react::BindingsInstallerHolder::javaobject> getBindingsInstaller(
|
|
52
|
+
jni::alias_ref<SymbioteEngineJni> /*self*/) {
|
|
53
|
+
return react::BindingsInstallerHolder::newObjectCxxArgs(
|
|
54
|
+
[](jsi::Runtime &runtime, const std::shared_ptr<react::CallInvoker> & /*callInvoker*/) {
|
|
55
|
+
installBindings(runtime);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static jdouble getVersion(jni::alias_ref<SymbioteEngineJni> /*self*/) {
|
|
60
|
+
return static_cast<jdouble>(kNativeVersion);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
} // namespace
|
|
65
|
+
|
|
66
|
+
} // namespace symbiote
|
|
67
|
+
|
|
68
|
+
// `System.loadLibrary("symbiote_engine")` from the module's own initializer is what gets here, so the
|
|
69
|
+
// natives are bound before anything can call them.
|
|
70
|
+
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void * /*reserved*/) {
|
|
71
|
+
return facebook::jni::initialize(vm, [] { symbiote::SymbioteEngineJni::registerNatives(); });
|
|
72
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package dev.symbiotenative.engine
|
|
2
|
+
|
|
3
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
6
|
+
import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
|
|
7
|
+
import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings
|
|
8
|
+
|
|
9
|
+
// Android's registration shim, and the twin of `core/engine/ios/SymbioteEngineModule.mm`. It carries
|
|
10
|
+
// no behaviour: creating it is what installs `global.__symbioteEngineNative`, and the payload lives
|
|
11
|
+
// in `core/engine/cpp/`, compiled unchanged by `../CMakeLists.txt`.
|
|
12
|
+
//
|
|
13
|
+
// `TurboModuleWithJSIBindings` is the Android analogue of iOS's `RCTTurboModuleWithJSIBindings` —
|
|
14
|
+
// `TurboModuleManager::getTurboModule` calls `getBindingsInstaller()` the moment it creates the Java
|
|
15
|
+
// module and applies the returned installer to the runtime. So there is no `install()` for JS to
|
|
16
|
+
// call, deliberately: an explicit install has an ordering requirement nothing enforces, while this
|
|
17
|
+
// hook cannot be skipped by anyone who reaches the module at all.
|
|
18
|
+
//
|
|
19
|
+
// Both members below are `external` and bound in `src/main/cpp/SymbioteEngineJni.cpp`. `getVersion`
|
|
20
|
+
// in particular: it is the ABI gate `native-engine.ts` reads, and its value is `kNativeVersion` in
|
|
21
|
+
// the shared C++ — restating it as a Kotlin constant would be a second spelling that drifts silently.
|
|
22
|
+
@DoNotStrip
|
|
23
|
+
@ReactModule(name = SymbioteEngineModule.NAME)
|
|
24
|
+
class SymbioteEngineModule(reactContext: ReactApplicationContext) :
|
|
25
|
+
NativeSymbioteEngineSpec(reactContext), TurboModuleWithJSIBindings {
|
|
26
|
+
|
|
27
|
+
// Not in a `companion object` initializer: the companion is only touched when something reads
|
|
28
|
+
// `NAME`, and `getBindingsInstaller` is called on the INSTANCE. Loading here runs before either
|
|
29
|
+
// native member can be reached, and a repeat `loadLibrary` of an already-loaded name is a no-op.
|
|
30
|
+
init {
|
|
31
|
+
System.loadLibrary("symbiote_engine")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@DoNotStrip external override fun getBindingsInstaller(): BindingsInstallerHolder
|
|
35
|
+
|
|
36
|
+
@DoNotStrip external override fun getVersion(): Double
|
|
37
|
+
|
|
38
|
+
companion object {
|
|
39
|
+
// Declared here rather than read off the generated spec's own `NAME`, so this file and the
|
|
40
|
+
// package beside it do not depend on a field of a class that exists only after codegen runs.
|
|
41
|
+
const val NAME: String = "SymbioteEngine"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package dev.symbiotenative.engine
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
// The one ReactPackage for `@symbiote-native/engine`'s native half. Autolinking registers it (see
|
|
10
|
+
// `../../../../../../react-native.config.cjs`), so no app touches its `MainApplication`.
|
|
11
|
+
//
|
|
12
|
+
// `BaseReactPackage` rather than the plain `ReactPackage` that `packages/android` uses, and
|
|
13
|
+
// `isTurboModule = true` rather than the legacy-interop default: those two together are what put the
|
|
14
|
+
// module on `TurboModuleManager::getTurboModule`, which is the ONLY path that consults
|
|
15
|
+
// `TurboModuleWithJSIBindings`. On the legacy path the module would be created, work, and install
|
|
16
|
+
// nothing — with no error anywhere.
|
|
17
|
+
class SymbioteEnginePackage : BaseReactPackage() {
|
|
18
|
+
|
|
19
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
20
|
+
if (name == SymbioteEngineModule.NAME) SymbioteEngineModule(reactContext) else null
|
|
21
|
+
|
|
22
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
23
|
+
mapOf(
|
|
24
|
+
SymbioteEngineModule.NAME to
|
|
25
|
+
ReactModuleInfo(
|
|
26
|
+
SymbioteEngineModule.NAME,
|
|
27
|
+
SymbioteEngineModule::class.java.name,
|
|
28
|
+
/* canOverrideExistingModule = */ false,
|
|
29
|
+
/* needsEagerInit = */ false,
|
|
30
|
+
/* isCxxModule = */ false,
|
|
31
|
+
/* isTurboModule = */ true,
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// files, not derived from this file's name. Mirrors
|
|
11
11
|
// RN's Libraries/Components/AccessibilityInfo/AccessibilityInfo.js.
|
|
12
12
|
import { isSymbioteNode } from '../node.js';
|
|
13
|
-
import { sendAccessibilityEvent as dispatchAccessibilityEvent } from '../
|
|
13
|
+
import { sendAccessibilityEvent as dispatchAccessibilityEvent } from '../imperative.js';
|
|
14
14
|
import { dlog } from '../debug.js';
|
|
15
15
|
export { isBoolean } from '../type-guards.js';
|
|
16
16
|
// The routing behind IAccessibilityInfoStatic.sendAccessibilityEvent, shared by both
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
export declare const ARIA_ALIAS_KEYS: readonly
|
|
2
|
-
/**
|
|
3
|
-
* Whether one key is an alias this fold consumes. `startsWith` rather than a Set lookup: this runs
|
|
4
|
-
* on `setProp`, the hottest write path in the engine (32 001 writes on one benchmark create), and
|
|
5
|
-
* it is guarded by the node's sticky flag so it is reached at most once per node per key. The
|
|
6
|
-
* `role` comparison comes first because it is the one alias with no prefix.
|
|
7
|
-
*/
|
|
8
|
-
export declare function isAriaAliasKey(key: string): boolean;
|
|
1
|
+
export declare const ARIA_ALIAS_KEYS: readonly ["role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"];
|
|
9
2
|
/**
|
|
10
3
|
* Fold the web-alias `aria-*` / `role` props into RN's canonical `accessibility*` props.
|
|
11
4
|
*
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
// RN's `aria-*` / `role` -> `accessibility*` fold, at the layer every path goes through.
|
|
2
2
|
//
|
|
3
|
-
// WHY IT IS HERE AND NOT IN A WRAPPER. It used to run inside each primitive's COMPONENT,
|
|
4
|
-
//
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
7
|
-
// (`REFUSAL_CATEGORIES.bagFold`). Accessibility props are ordinary in real code, so that refusal
|
|
8
|
-
// cost lowering coverage on every primitive, including the three already lowered. Moving the fold
|
|
9
|
-
// down deletes the refusal instead of teaching four transforms a bag operation they cannot express.
|
|
3
|
+
// WHY IT IS HERE AND NOT IN A WRAPPER. It used to run inside each primitive's COMPONENT, and a tag
|
|
4
|
+
// has none. It also cannot run per attribute — `aria-checked` has to be folded against a sibling
|
|
5
|
+
// `accessibilityState` — so it belongs at the one point where the whole bag is known, which is the
|
|
6
|
+
// payload build.
|
|
10
7
|
//
|
|
11
8
|
// IT IS A MOVE, NOT A REWRITE, AND THAT IS DELIBERATE. The function carries TWO CONTRADICTORY
|
|
12
9
|
// PRECEDENCE RULES: for the scalars an explicit `accessibility*` WINS and the alias only fills a
|
|
@@ -59,6 +56,10 @@ const ROLE_TO_ACCESSIBILITY_ROLE = {
|
|
|
59
56
|
// list. `slotDerived` (host-behavior.ts) takes prop NAMES, so a primitive whose payload derives
|
|
60
57
|
// from an owner's aria props has to enumerate them — and a second hand-written copy is exactly what
|
|
61
58
|
// `.claude/rules/adapter-parity-audit.md` records going stale one member at a time.
|
|
59
|
+
// `as const` rather than `readonly string[]`, so the members are LITERALS. That is what lets a
|
|
60
|
+
// consumer index a prop type with them — `pickAccessibilityProps` in the Svelte adapter forwards the
|
|
61
|
+
// aria half by looping this list — and it makes the two sides check each other: a name here that is
|
|
62
|
+
// not a key of `IAriaProps` stops compiling at the use site rather than going quietly unforwarded.
|
|
62
63
|
export const ARIA_ALIAS_KEYS = [
|
|
63
64
|
'role',
|
|
64
65
|
'aria-label',
|
|
@@ -85,15 +86,11 @@ function hasAnyAriaKey(props) {
|
|
|
85
86
|
}
|
|
86
87
|
return false;
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*/
|
|
94
|
-
export function isAriaAliasKey(key) {
|
|
95
|
-
return key === 'role' || key.startsWith('aria-');
|
|
96
|
-
}
|
|
89
|
+
// `isAriaAliasKey` WAS HERE AND IS GONE (2026-09-18). Its whole reason was to maintain
|
|
90
|
+
// `node.hasAriaAlias` from `setProp` — the hottest write path in the engine, 32 001 writes on one
|
|
91
|
+
// benchmark create — and that flag existed only to gate the aria fold inside the headless payload
|
|
92
|
+
// builder. The fold left for `SymbioteFabricProps.cpp`, which recomputes presence from the bag it
|
|
93
|
+
// holds, so the flag became write-only and this function became its only maintainer. Both went.
|
|
97
94
|
function isRecord(value) {
|
|
98
95
|
return typeof value === 'object' && value !== null;
|
|
99
96
|
}
|
|
@@ -26,7 +26,7 @@ export declare function decay(value: AnimatedValue, config: IDecayConfig): IComp
|
|
|
26
26
|
export interface IParallelConfig {
|
|
27
27
|
stopTogether?: boolean;
|
|
28
28
|
}
|
|
29
|
-
export declare function parallel(animations: ICompositeAnimation[], config?: IParallelConfig): ICompositeAnimation;
|
|
29
|
+
export declare function parallel(animations: (ICompositeAnimation | undefined)[], config?: IParallelConfig): ICompositeAnimation;
|
|
30
30
|
export declare function sequence(animations: ICompositeAnimation[]): ICompositeAnimation;
|
|
31
31
|
export declare function delay(time: number): ICompositeAnimation;
|
|
32
32
|
export declare function stagger(time: number, animations: ICompositeAnimation[]): ICompositeAnimation;
|
|
@@ -99,7 +99,10 @@ export function decay(value, config) {
|
|
|
99
99
|
},
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
-
export function parallel(
|
|
102
|
+
export function parallel(
|
|
103
|
+
// `| undefined` entries: `AnimatedImplementation.js`'s `parallelImpl` treats a falsy array
|
|
104
|
+
// entry (e.g. `cond && timing(...)`) as already-finished rather than crashing.
|
|
105
|
+
animations, config) {
|
|
103
106
|
let doneCount = 0;
|
|
104
107
|
// Track per-animation completion so stop() calls each at most once.
|
|
105
108
|
const hasEnded = {};
|
|
@@ -123,19 +126,24 @@ export function parallel(animations, config) {
|
|
|
123
126
|
result.stop();
|
|
124
127
|
}
|
|
125
128
|
};
|
|
126
|
-
animation
|
|
129
|
+
if (animation === undefined) {
|
|
130
|
+
cb({ finished: true });
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
animation.start(cb, isLooping);
|
|
134
|
+
}
|
|
127
135
|
});
|
|
128
136
|
},
|
|
129
137
|
stop() {
|
|
130
138
|
animations.forEach((animation, idx) => {
|
|
131
139
|
if (!hasEnded[idx])
|
|
132
|
-
animation
|
|
140
|
+
animation?.stop();
|
|
133
141
|
hasEnded[idx] = true;
|
|
134
142
|
});
|
|
135
143
|
},
|
|
136
144
|
reset() {
|
|
137
145
|
animations.forEach((animation, idx) => {
|
|
138
|
-
animation
|
|
146
|
+
animation?.reset();
|
|
139
147
|
hasEnded[idx] = false;
|
|
140
148
|
doneCount = 0;
|
|
141
149
|
});
|
|
@@ -192,6 +200,12 @@ export function delay(time) {
|
|
|
192
200
|
export function stagger(time, animations) {
|
|
193
201
|
return parallel(animations.map((animation, i) => sequence([delay(time * i), animation])));
|
|
194
202
|
}
|
|
203
|
+
// TODO(rn-parity, low priority): vendor's `loopImpl`/`sequenceImpl`/`parallelImpl` each throw
|
|
204
|
+
// ("Loops run using the native driver cannot contain Animated.X animations") from a
|
|
205
|
+
// `_startNativeLoop` reachable only by nesting `loop(loop(...))` around a native-driven leaf. Ours
|
|
206
|
+
// has no such throw: the nesting silently falls back to the JS restart loop instead of erroring.
|
|
207
|
+
// Judged non-actionable (misuse-detection guard, no runtime-visible effect for a correct caller,
|
|
208
|
+
// same class as the `numColumns` invariant) — see the audit skill for the full reasoning.
|
|
195
209
|
export function loop(animation, config = {}) {
|
|
196
210
|
const iterations = config.iterations ?? -1;
|
|
197
211
|
const resetBeforeIteration = config.resetBeforeIteration ?? true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type IEasingFunction = (t: number) => number;
|
|
2
|
-
export
|
|
2
|
+
export interface IEasing {
|
|
3
3
|
step0(n: number): number;
|
|
4
4
|
step1(n: number): number;
|
|
5
5
|
linear(t: number): number;
|
|
@@ -17,4 +17,5 @@ export declare const Easing: {
|
|
|
17
17
|
in(easing: IEasingFunction): IEasingFunction;
|
|
18
18
|
out(easing: IEasingFunction): IEasingFunction;
|
|
19
19
|
inOut(easing: IEasingFunction): IEasingFunction;
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
|
+
export declare const Easing: IEasing;
|
package/build/animated/easing.js
CHANGED
|
@@ -1,88 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (cachedEase === undefined) {
|
|
19
|
-
cachedEase = bezier(0.42, 0, 1, 1);
|
|
20
|
-
}
|
|
21
|
-
return cachedEase(t);
|
|
22
|
-
},
|
|
23
|
-
quad(t) {
|
|
24
|
-
return t * t;
|
|
25
|
-
},
|
|
26
|
-
cubic(t) {
|
|
27
|
-
return t * t * t;
|
|
28
|
-
},
|
|
29
|
-
// Nth-power: position is the Nth power of elapsed time.
|
|
30
|
-
poly(n) {
|
|
31
|
-
return t => Math.pow(t, n);
|
|
32
|
-
},
|
|
33
|
-
sin(t) {
|
|
34
|
-
return 1 - Math.cos((t * Math.PI) / 2);
|
|
35
|
-
},
|
|
36
|
-
circle(t) {
|
|
37
|
-
return 1 - Math.sqrt(1 - t * t);
|
|
38
|
-
},
|
|
39
|
-
exp(t) {
|
|
40
|
-
return Math.pow(2, 10 * (t - 1));
|
|
41
|
-
},
|
|
42
|
-
// Elastic: spring-like oscillation. bounciness 1 overshoots once; 0 not at all.
|
|
43
|
-
elastic(bounciness = 1) {
|
|
44
|
-
const p = bounciness * Math.PI;
|
|
45
|
-
return t => 1 - Math.pow(Math.cos((t * Math.PI) / 2), 3) * Math.cos(t * p);
|
|
46
|
-
},
|
|
47
|
-
// Animates back slightly before moving forward.
|
|
48
|
-
back(s = 1.70158) {
|
|
49
|
-
return t => t * t * ((s + 1) * t - s);
|
|
50
|
-
},
|
|
51
|
-
// A simple bouncing effect.
|
|
52
|
-
bounce(t) {
|
|
53
|
-
if (t < 1 / 2.75) {
|
|
54
|
-
return 7.5625 * t * t;
|
|
55
|
-
}
|
|
56
|
-
if (t < 2 / 2.75) {
|
|
57
|
-
const t2 = t - 1.5 / 2.75;
|
|
58
|
-
return 7.5625 * t2 * t2 + 0.75;
|
|
59
|
-
}
|
|
60
|
-
if (t < 2.5 / 2.75) {
|
|
61
|
-
const t2 = t - 2.25 / 2.75;
|
|
62
|
-
return 7.5625 * t2 * t2 + 0.9375;
|
|
63
|
-
}
|
|
64
|
-
const t2 = t - 2.625 / 2.75;
|
|
65
|
-
return 7.5625 * t2 * t2 + 0.984375;
|
|
66
|
-
},
|
|
67
|
-
// A cubic bezier curve, same as CSS transition-timing-function.
|
|
68
|
-
bezier(x1, y1, x2, y2) {
|
|
69
|
-
return bezier(x1, y1, x2, y2);
|
|
70
|
-
},
|
|
71
|
-
// Runs an easing function forwards.
|
|
72
|
-
in(easing) {
|
|
73
|
-
return easing;
|
|
74
|
-
},
|
|
75
|
-
// Runs an easing function backwards.
|
|
76
|
-
out(easing) {
|
|
77
|
-
return t => 1 - easing(1 - t);
|
|
78
|
-
},
|
|
79
|
-
// Makes any easing function symmetrical (forwards then backwards).
|
|
80
|
-
inOut(easing) {
|
|
81
|
-
return t => {
|
|
82
|
-
if (t < 0.5) {
|
|
83
|
-
return easing(t * 2) / 2;
|
|
84
|
-
}
|
|
85
|
-
return 1 - easing((1 - t) * 2) / 2;
|
|
86
|
-
};
|
|
87
|
-
},
|
|
88
|
-
};
|
|
1
|
+
// RN's own Easing, imported rather than ported.
|
|
2
|
+
//
|
|
3
|
+
// The port this replaces was 109 lines beside a 142-line `bezier.ts`, and it reproduced upstream
|
|
4
|
+
// formula for formula. Swept at 20 001 points per function before the swap: 15 of the 17 agree to
|
|
5
|
+
// the bit, and the two bezier-backed ones (`ease`, `bezier`) differ by 2.9e-10, because upstream
|
|
6
|
+
// samples its spline into a Float32Array where the port used a plain (float64) array. That is
|
|
7
|
+
// below a sub-pixel on any animation this drives.
|
|
8
|
+
//
|
|
9
|
+
// The TYPE is ours and stays ours. `Easing` is on the engine barrel and re-exported by all five
|
|
10
|
+
// adapters, and the upstream module ships no types - so importing it untyped would hand every
|
|
11
|
+
// consumer `any`. Twenty lines of interface in exchange for 251 lines of implementation.
|
|
12
|
+
import EasingUpstream from 'react-native/Libraries/Animated/Easing';
|
|
13
|
+
// tsc resolves this deep path but reads the Flow source through allowJs, so the default export
|
|
14
|
+
// arrives shapeless rather than untyped - the mismatch is the only thing the suppression covers,
|
|
15
|
+
// and IEasing above is what every consumer actually sees.
|
|
16
|
+
// @ts-expect-error - upstream ships no usable types for this path.
|
|
17
|
+
export const Easing = EasingUpstream;
|