@tamagui/native-registry 0.0.0-bootstrap.0 → 3.0.0-beta.643.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.
Files changed (81) hide show
  1. package/README.md +58 -0
  2. package/TamaguiRegistry.podspec +32 -0
  3. package/android/CMakeLists.txt +25 -0
  4. package/android/build.gradle +83 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  7. package/android/src/main/java/com/tamagui/registry/TamaguiRegistryPackage.kt +25 -0
  8. package/cpp/HybridTamaguiRegistry.cpp +369 -0
  9. package/cpp/HybridTamaguiRegistry.hpp +117 -0
  10. package/dist/cjs/index.cjs +73 -0
  11. package/dist/cjs/index.native.cjs +144 -0
  12. package/dist/cjs/index.native.js +146 -0
  13. package/dist/cjs/index.native.js.map +1 -0
  14. package/dist/cjs/mirror.cjs +44 -0
  15. package/dist/cjs/mirror.native.cjs +48 -0
  16. package/dist/cjs/mirror.native.js +50 -0
  17. package/dist/cjs/mirror.native.js.map +1 -0
  18. package/dist/cjs/processStyleColors.cjs +56 -0
  19. package/dist/cjs/processStyleColors.native.cjs +58 -0
  20. package/dist/cjs/processStyleColors.native.js +60 -0
  21. package/dist/cjs/processStyleColors.native.js.map +1 -0
  22. package/dist/cjs/specs/TamaguiRegistry.nitro.cjs +17 -0
  23. package/dist/cjs/specs/TamaguiRegistry.nitro.native.cjs +19 -0
  24. package/dist/cjs/specs/TamaguiRegistry.nitro.native.js +21 -0
  25. package/dist/cjs/specs/TamaguiRegistry.nitro.native.js.map +1 -0
  26. package/dist/cjs/types.cjs +17 -0
  27. package/dist/cjs/types.native.cjs +19 -0
  28. package/dist/cjs/types.native.js +21 -0
  29. package/dist/cjs/types.native.js.map +1 -0
  30. package/dist/esm/index.js +39 -0
  31. package/dist/esm/index.js.map +1 -0
  32. package/dist/esm/index.mjs +39 -0
  33. package/dist/esm/index.mjs.map +1 -0
  34. package/dist/esm/index.native.js +107 -0
  35. package/dist/esm/index.native.js.map +1 -0
  36. package/dist/esm/mirror.mjs +17 -0
  37. package/dist/esm/mirror.mjs.map +1 -0
  38. package/dist/esm/mirror.native.js +19 -0
  39. package/dist/esm/mirror.native.js.map +1 -0
  40. package/dist/esm/processStyleColors.mjs +36 -0
  41. package/dist/esm/processStyleColors.mjs.map +1 -0
  42. package/dist/esm/processStyleColors.native.js +36 -0
  43. package/dist/esm/processStyleColors.native.js.map +1 -0
  44. package/dist/esm/specs/TamaguiRegistry.nitro.mjs +0 -0
  45. package/dist/esm/specs/TamaguiRegistry.nitro.native.js +0 -0
  46. package/dist/esm/types.mjs +0 -0
  47. package/dist/esm/types.native.js +0 -0
  48. package/nitro.json +18 -0
  49. package/nitrogen/generated/.gitattributes +1 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tamagui/registry/tamaguiregistryOnLoad.kt +35 -0
  51. package/nitrogen/generated/android/tamaguiregistry+autolinking.cmake +81 -0
  52. package/nitrogen/generated/android/tamaguiregistry+autolinking.gradle +27 -0
  53. package/nitrogen/generated/android/tamaguiregistryOnLoad.cpp +49 -0
  54. package/nitrogen/generated/android/tamaguiregistryOnLoad.hpp +34 -0
  55. package/nitrogen/generated/ios/TamaguiRegistry+autolinking.rb +62 -0
  56. package/nitrogen/generated/ios/TamaguiRegistry-Swift-Cxx-Bridge.cpp +17 -0
  57. package/nitrogen/generated/ios/TamaguiRegistry-Swift-Cxx-Bridge.hpp +27 -0
  58. package/nitrogen/generated/ios/TamaguiRegistry-Swift-Cxx-Umbrella.hpp +38 -0
  59. package/nitrogen/generated/ios/TamaguiRegistryAutolinking.mm +35 -0
  60. package/nitrogen/generated/ios/TamaguiRegistryAutolinking.swift +16 -0
  61. package/nitrogen/generated/shared/c++/HybridTamaguiRegistrySpec.cpp +27 -0
  62. package/nitrogen/generated/shared/c++/HybridTamaguiRegistrySpec.hpp +68 -0
  63. package/package.json +69 -6
  64. package/src/index.native.ts +174 -0
  65. package/src/index.ts +79 -0
  66. package/src/mirror.ts +31 -0
  67. package/src/processStyleColors.ts +42 -0
  68. package/src/specs/TamaguiRegistry.nitro.ts +51 -0
  69. package/src/types.ts +65 -0
  70. package/types/index.d.ts +20 -0
  71. package/types/index.d.ts.map +11 -0
  72. package/types/index.native.d.ts +37 -0
  73. package/types/index.native.d.ts.map +11 -0
  74. package/types/mirror.d.ts +7 -0
  75. package/types/mirror.d.ts.map +11 -0
  76. package/types/processStyleColors.d.ts +3 -0
  77. package/types/processStyleColors.d.ts.map +11 -0
  78. package/types/specs/TamaguiRegistry.nitro.d.ts +49 -0
  79. package/types/specs/TamaguiRegistry.nitro.d.ts.map +11 -0
  80. package/types/types.d.ts +61 -0
  81. package/types/types.d.ts.map +11 -0
package/README.md CHANGED
@@ -1 +1,59 @@
1
1
  # @tamagui/native-registry
2
+
3
+ `@tamagui/native-registry` is the native engine for Tamagui's experimental
4
+ theme fast path. It links eligible Fabric views to a small C++ registry and
5
+ applies theme changes through one ShadowTree commit without re-rendering those
6
+ views in React.
7
+
8
+ The package supports React Native 0.82 and newer. It uses
9
+ `react-native-nitro-modules`, so adding it requires rebuilding the native app.
10
+
11
+ ## Enable the experiment
12
+
13
+ Install both native packages:
14
+
15
+ ```sh
16
+ bun add @tamagui/native-registry react-native-nitro-modules
17
+ ```
18
+
19
+ Enable compiler output in `tamagui.build.ts`:
20
+
21
+ ```ts
22
+ import type { TamaguiBuildOptions } from 'tamagui'
23
+
24
+ export default {
25
+ config: './tamagui.config.ts',
26
+ components: ['tamagui'],
27
+ experimental: {
28
+ nativeFastPath: true,
29
+ },
30
+ } satisfies TamaguiBuildOptions
31
+ ```
32
+
33
+ Install the engine before the themed application tree mounts:
34
+
35
+ ```ts
36
+ import * as nativeRegistry from '@tamagui/native-registry'
37
+ import { setNativeStyleEngine } from 'tamagui'
38
+
39
+ if (nativeRegistry.isAvailable()) {
40
+ setNativeStyleEngine(nativeRegistry)
41
+ }
42
+ ```
43
+
44
+ Then rebuild the iOS and Android applications. A Metro reload cannot add the
45
+ native module to an existing binary.
46
+
47
+ This API is experimental. Keep the flag off for production applications unless
48
+ you can run the native correctness suite for the React Native versions and list
49
+ implementations your application ships.
50
+
51
+ ## Fallback behavior
52
+
53
+ The flag defaults to `false`. With the flag off, native compiler output is
54
+ unchanged. With the flag on but no engine installed, emitted components use the
55
+ existing theme hook and React render path. Web output is unchanged in both
56
+ cases.
57
+
58
+ The registry coexists with Tamagui's existing `DynamicColorIOS` optimization.
59
+ It does not replace or remove that path.
@@ -0,0 +1,32 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "TamaguiRegistry"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["repository"]["url"]
10
+ s.license = package["license"]
11
+ s.authors = "Tamagui"
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
15
+
16
+ s.source_files = [
17
+ "cpp/**/*.{h,hpp,cpp}",
18
+ ]
19
+
20
+ s.pod_target_xcconfig = {
21
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
22
+ "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES FOLLY_MOBILE",
23
+ }
24
+
25
+ # nitrogen autolinking (adds NitroModules dep + generated sources)
26
+ load "nitrogen/generated/ios/TamaguiRegistry+autolinking.rb"
27
+ add_nitrogen_files(s)
28
+
29
+ s.dependency "React-jsi"
30
+ s.dependency "React-callinvoker"
31
+ install_modules_dependencies(s)
32
+ end
@@ -0,0 +1,25 @@
1
+ project(TamaguiRegistry)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME tamaguiregistry)
5
+ set(CMAKE_CXX_STANDARD 20)
6
+
7
+ add_library(${PACKAGE_NAME} SHARED
8
+ src/main/cpp/cpp-adapter.cpp
9
+ ../cpp/HybridTamaguiRegistry.cpp
10
+ )
11
+
12
+ # nitrogen autolinking (generated sources + NitroModules linkage)
13
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/tamaguiregistry+autolinking.cmake)
14
+
15
+ include_directories(
16
+ "../cpp"
17
+ )
18
+
19
+ find_library(LOG_LIB log)
20
+
21
+ target_link_libraries(
22
+ ${PACKAGE_NAME}
23
+ ${LOG_LIB}
24
+ android
25
+ )
@@ -0,0 +1,83 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath("com.android.tools.build:gradle:8.1.1")
13
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.9.0')}")
14
+ }
15
+ }
16
+
17
+ apply plugin: "com.android.library"
18
+ apply plugin: "org.jetbrains.kotlin.android"
19
+
20
+ // nitrogen autolinking
21
+ apply from: "${project.projectDir}/../nitrogen/generated/android/tamaguiregistry+autolinking.gradle"
22
+
23
+ def safeExtGet(prop, fallback) {
24
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
25
+ }
26
+
27
+ android {
28
+ namespace = "com.tamagui.registry"
29
+ compileSdkVersion safeExtGet('compileSdkVersion', 35)
30
+
31
+ defaultConfig {
32
+ minSdkVersion safeExtGet('minSdkVersion', 24)
33
+ targetSdkVersion safeExtGet('targetSdkVersion', 35)
34
+
35
+ externalNativeBuild {
36
+ cmake {
37
+ cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
38
+ arguments "-DANDROID_STL=c++_shared"
39
+ abiFilters(*(rootProject.getProperties().get("reactNativeArchitectures")?.split(",")
40
+ ?: ["x86", "x86_64", "armeabi-v7a", "arm64-v8a"]))
41
+ }
42
+ }
43
+ }
44
+
45
+ externalNativeBuild {
46
+ cmake {
47
+ path "CMakeLists.txt"
48
+ }
49
+ }
50
+
51
+ buildFeatures {
52
+ prefab true
53
+ }
54
+
55
+ // same exclusion list as react-native-nitro-modules: React Native's own AAR
56
+ // already ships every one of these, and packaging a second copy fails the
57
+ // app's mergeDebugAndroidTestNativeLibs with a duplicate-path error
58
+ packagingOptions {
59
+ excludes = [
60
+ "META-INF",
61
+ "META-INF/**",
62
+ "**/libc++_shared.so",
63
+ "**/libfbjni.so",
64
+ "**/libjsi.so",
65
+ "**/libfolly_json.so",
66
+ "**/libfolly_runtime.so",
67
+ "**/libglog.so",
68
+ "**/libhermes.so",
69
+ "**/libhermes-executor-debug.so",
70
+ "**/libhermes_executor.so",
71
+ "**/libreactnative.so",
72
+ "**/libreactnativejni.so",
73
+ "**/libturbomodulejsijni.so",
74
+ "**/libreact_nativemodule_core.so",
75
+ "**/libjscexecutor.so",
76
+ ]
77
+ }
78
+ }
79
+
80
+ dependencies {
81
+ implementation 'com.facebook.react:react-native:+'
82
+ implementation project(':react-native-nitro-modules')
83
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,11 @@
1
+ #include <fbjni/fbjni.h>
2
+ #include <jni.h>
3
+
4
+ #include "tamaguiregistryOnLoad.hpp"
5
+
6
+ // called by System.loadLibrary("tamaguiregistry"); registers all nitro hybrids
7
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
8
+ return facebook::jni::initialize(vm, []() {
9
+ margelo::nitro::tamagui::registry::registerAllNatives();
10
+ });
11
+ }
@@ -0,0 +1,25 @@
1
+ package com.tamagui.registry
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.ReactModuleInfoProvider
7
+ import com.margelo.nitro.tamagui.registry.tamaguiregistryOnLoad
8
+
9
+ // no react modules: this package exists so autolinking loads the C++ library,
10
+ // which registers the TamaguiRegistry nitro hybrid on JNI_OnLoad
11
+ class TamaguiRegistryPackage : BaseReactPackage() {
12
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
13
+ return null
14
+ }
15
+
16
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
17
+ return ReactModuleInfoProvider { HashMap() }
18
+ }
19
+
20
+ companion object {
21
+ init {
22
+ tamaguiregistryOnLoad.initializeNative()
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,369 @@
1
+ #include "HybridTamaguiRegistry.hpp"
2
+
3
+ #include <jsi/JSIDynamic.h>
4
+ #include <react/renderer/components/root/RootShadowNode.h>
5
+ #include <react/renderer/core/PropsParserContext.h>
6
+ #include <react/renderer/core/RawProps.h>
7
+ #include <react/renderer/core/ShadowNode.h>
8
+ #include <react/renderer/core/ShadowNodeFragment.h>
9
+ #include <react/renderer/mounting/ShadowTree.h>
10
+ #include <react/renderer/mounting/ShadowTreeRegistry.h>
11
+ #include <react/renderer/uimanager/UIManager.h>
12
+ #include <react/renderer/uimanager/UIManagerBinding.h>
13
+ #include <react/renderer/uimanager/primitives.h>
14
+
15
+ #include <unordered_set>
16
+
17
+ namespace margelo::nitro::tamagui::registry {
18
+
19
+ using namespace facebook::react;
20
+ using margelo::nitro::Prototype;
21
+
22
+ void HybridTamaguiRegistry::loadHybridMethods() {
23
+ HybridTamaguiRegistrySpec::loadHybridMethods();
24
+ registerHybrids(this, [](Prototype& prototype) {
25
+ prototype.registerRawHybridMethod("link", 3, &HybridTamaguiRegistry::link);
26
+ prototype.registerRawHybridMethod("applyViewStates", 1,
27
+ &HybridTamaguiRegistry::applyViewStates);
28
+ prototype.registerRawHybridMethod(
29
+ "updateViewStateTables", 1,
30
+ &HybridTamaguiRegistry::updateViewStateTables);
31
+ prototype.registerRawHybridMethod("getViewState", 1,
32
+ &HybridTamaguiRegistry::getViewState);
33
+ });
34
+ }
35
+
36
+ jsi::Value HybridTamaguiRegistry::link(jsi::Runtime& rt, const jsi::Value&,
37
+ const jsi::Value* args, size_t count) {
38
+ if (count < 3) {
39
+ throw jsi::JSError(rt, "link(shadowNode, slots, scopeId) requires 3 arguments");
40
+ }
41
+ if (!args[0].isObject()) return jsi::Value::undefined();
42
+ auto obj = args[0].asObject(rt);
43
+ if (!obj.hasNativeState(rt)) return jsi::Value::undefined();
44
+ auto wrapper = obj.getNativeState<ShadowNodeWrapper>(rt);
45
+ if (!wrapper || !wrapper->shadowNode) return jsi::Value::undefined();
46
+
47
+ runtime_ = &rt;
48
+
49
+ auto slots = jsi::dynamicFromValue(rt, args[1]);
50
+ const double id = nextId_++;
51
+ views_[id] = LinkedView{
52
+ wrapper->shadowNode,
53
+ args[2].asString(rt).utf8(rt),
54
+ slots.getDefault("base", nullptr),
55
+ slots.getDefault("state", nullptr),
56
+ };
57
+ return jsi::Value(id);
58
+ }
59
+
60
+ void HybridTamaguiRegistry::unlink(double id) {
61
+ views_.erase(id);
62
+ }
63
+
64
+ const std::string* HybridTamaguiRegistry::activeStateName(
65
+ const std::string& scopeId) const {
66
+ if (auto it = scopeStates_.find(scopeId); it != scopeStates_.end()) {
67
+ return &it->second;
68
+ }
69
+ if (!scopeId.empty()) {
70
+ if (auto root = scopeStates_.find(""); root != scopeStates_.end()) {
71
+ return &root->second;
72
+ }
73
+ }
74
+ return nullptr;
75
+ }
76
+
77
+ void HybridTamaguiRegistry::setStateName(const std::string& scopeId,
78
+ const std::string& stateName) {
79
+ auto it = scopeStates_.find(scopeId);
80
+ if (it != scopeStates_.end() && it->second == stateName) return;
81
+ scopeStates_[scopeId] = stateName;
82
+ if (runtime_) {
83
+ // a root change also affects scoped views inheriting root, so commit all
84
+ applyUpdates(*runtime_, scopeId.empty() ? nullptr : &scopeId);
85
+ }
86
+ }
87
+
88
+ std::string HybridTamaguiRegistry::getStateName(const std::string& scopeId) {
89
+ const auto* name = activeStateName(scopeId);
90
+ return name ? *name : "";
91
+ }
92
+
93
+ void HybridTamaguiRegistry::removeScope(const std::string& scopeId) {
94
+ scopeStates_.erase(scopeId);
95
+ }
96
+
97
+ double HybridTamaguiRegistry::getViewCount() {
98
+ return static_cast<double>(views_.size());
99
+ }
100
+
101
+ double HybridTamaguiRegistry::getCommitCount() {
102
+ return commitCount_;
103
+ }
104
+
105
+ double HybridTamaguiRegistry::getMissCount() {
106
+ return missCount_;
107
+ }
108
+
109
+ // ── In-transaction tree cloning ──
110
+ //
111
+ // RN's UIManager::updateShadowTree (0.81+) pre-builds its clone from a
112
+ // revision read before the commit and returns it from the commit callback
113
+ // unchanged, so a commit that lands in between (a same-tick React render,
114
+ // a Reanimated frame) makes it commit a stale-based tree: updates get lost
115
+ // or revert. Phase 0 reproduced exactly that. So the engine does its own
116
+ // ShadowTree::commit and builds the clone INSIDE the transaction from the
117
+ // root the callback receives; on retry the clone is rebuilt from the fresh
118
+ // root and the race disappears.
119
+
120
+ namespace {
121
+
122
+ using LeafUpdates = HybridTamaguiRegistry::LeafUpdates;
123
+ using AffectedNodes =
124
+ std::unordered_map<const ShadowNodeFamily*, std::unordered_set<size_t>>;
125
+
126
+ AffectedNodes findAffectedNodes(const RootShadowNode& root,
127
+ const LeafUpdates& updates) {
128
+ AffectedNodes affected;
129
+ for (const auto& [family, props] : updates) {
130
+ // families of unmounted views have no ancestors in this root: skipped
131
+ for (const auto& [parentNode, childIndex] : family->getAncestors(root)) {
132
+ affected[&parentNode.get().getFamily()].insert(childIndex);
133
+ }
134
+ }
135
+ return affected;
136
+ }
137
+
138
+ std::shared_ptr<ShadowNode> cloneSubtree(const ShadowNode& node,
139
+ const LeafUpdates& updates,
140
+ const AffectedNodes& affected) {
141
+ const auto* family = &node.getFamily();
142
+ auto children = node.getChildren();
143
+
144
+ if (auto it = affected.find(family); it != affected.end()) {
145
+ for (auto index : it->second) {
146
+ children[index] = cloneSubtree(*children[index], updates, affected);
147
+ }
148
+ }
149
+
150
+ Props::Shared newProps = ShadowNodeFragment::propsPlaceholder();
151
+ if (auto it = updates.find(family); it != updates.end()) {
152
+ PropsParserContext ctx{node.getSurfaceId(), *node.getContextContainer()};
153
+ newProps = node.getComponentDescriptor().cloneProps(ctx, node.getProps(),
154
+ RawProps(it->second));
155
+ }
156
+
157
+ return node.clone({
158
+ .props = newProps,
159
+ .children = std::make_shared<std::vector<std::shared_ptr<const ShadowNode>>>(
160
+ std::move(children)),
161
+ .state = node.getState(),
162
+ });
163
+ }
164
+
165
+ } // namespace
166
+
167
+ // merge base + the named state's props for one view into `out`, syncing the
168
+ // family's native props so React re-commits of unchanged children merge our
169
+ // values instead of resurrecting a stale absorbed node (phase 0: the JS
170
+ // mirror alone provably does not cover this). returns false on a state miss.
171
+ bool HybridTamaguiRegistry::buildViewUpdate(LinkedView& view,
172
+ const std::string& stateName,
173
+ LeafUpdates& out) {
174
+ folly::dynamic props =
175
+ view.baseProps.isObject() ? view.baseProps : folly::dynamic::object();
176
+
177
+ if (view.stateProps.isObject()) {
178
+ auto it = view.stateProps.find(stateName);
179
+ if (it == view.stateProps.items().end()) {
180
+ missCount_ += 1;
181
+ return false;
182
+ }
183
+ if (props.empty()) {
184
+ props = it->second;
185
+ } else {
186
+ props.update(it->second);
187
+ }
188
+ } else if (props.empty()) {
189
+ return false;
190
+ }
191
+
192
+ const auto& family = view.node->getFamily();
193
+ if (family.nativeProps_DEPRECATED) {
194
+ family.nativeProps_DEPRECATED->update(props);
195
+ } else {
196
+ family.nativeProps_DEPRECATED = std::make_unique<folly::dynamic>(props);
197
+ }
198
+
199
+ // a null value means reset-to-default (a style key dropped by a media
200
+ // change): it must COMMIT as null so RawProps clears the prop, but must not
201
+ // stick in nativeProps_DEPRECATED or every future React render would keep
202
+ // re-clearing a key the render may now legitimately set
203
+ auto& sticky = *family.nativeProps_DEPRECATED;
204
+ for (const auto& item : props.items()) {
205
+ if (item.second.isNull()) sticky.erase(item.first.asString());
206
+ }
207
+
208
+ // the retained node keeps its family alive; unmounted views resolve to
209
+ // no ancestors during the transaction and are skipped safely
210
+ out[&family] = std::move(props);
211
+ return true;
212
+ }
213
+
214
+ void HybridTamaguiRegistry::commitUpdates(jsi::Runtime& rt,
215
+ LeafUpdates& updates) {
216
+ if (updates.empty()) return;
217
+
218
+ auto binding = UIManagerBinding::getBinding(rt);
219
+ if (!binding) return;
220
+
221
+ binding->getUIManager().getShadowTreeRegistry().enumerate(
222
+ [this, &updates](const ShadowTree& tree, bool& /*stop*/) {
223
+ auto status = tree.commit(
224
+ [&updates](const RootShadowNode& oldRoot)
225
+ -> RootShadowNode::Unshared {
226
+ auto affected = findAffectedNodes(oldRoot, updates);
227
+ if (affected.empty()) return nullptr;
228
+ return std::static_pointer_cast<RootShadowNode>(
229
+ cloneSubtree(oldRoot, updates, affected));
230
+ },
231
+ {.enableStateReconciliation = false, .mountSynchronously = true});
232
+ if (status == ShadowTree::CommitStatus::Succeeded) {
233
+ commitCount_ += 1;
234
+ }
235
+ });
236
+ }
237
+
238
+ void HybridTamaguiRegistry::applyUpdates(jsi::Runtime& rt,
239
+ const std::string* scopeFilter) {
240
+ LeafUpdates updates;
241
+ updates.reserve(views_.size());
242
+
243
+ for (auto& [id, view] : views_) {
244
+ if (scopeFilter && view.scopeId != *scopeFilter) continue;
245
+ // runtime-managed views (applyViewStates) resolve state per view; scope
246
+ // broadcasts must not fight their controller
247
+ if (!view.activeState.empty()) continue;
248
+
249
+ const auto* name =
250
+ view.stateProps.isObject() ? activeStateName(view.scopeId) : nullptr;
251
+ if (view.stateProps.isObject() && !name) continue;
252
+
253
+ buildViewUpdate(view, name ? *name : "", updates);
254
+ }
255
+
256
+ commitUpdates(rt, updates);
257
+ }
258
+
259
+ jsi::Value HybridTamaguiRegistry::applyViewStates(jsi::Runtime& rt,
260
+ const jsi::Value&,
261
+ const jsi::Value* args,
262
+ size_t count) {
263
+ if (count < 1 || !args[0].isObject()) {
264
+ throw jsi::JSError(rt, "applyViewStates(entries) requires an array");
265
+ }
266
+ runtime_ = &rt;
267
+
268
+ auto entries = jsi::dynamicFromValue(rt, args[0]);
269
+ if (!entries.isArray()) {
270
+ throw jsi::JSError(rt, "applyViewStates(entries) requires an array");
271
+ }
272
+
273
+ LeafUpdates updates;
274
+ updates.reserve(entries.size());
275
+
276
+ for (auto& entry : entries) {
277
+ if (!entry.isObject()) continue;
278
+ auto idIt = entry.find("id");
279
+ auto stateIt = entry.find("state");
280
+ if (idIt == entry.items().end() || stateIt == entry.items().end()) continue;
281
+
282
+ auto viewIt = views_.find(idIt->second.asDouble());
283
+ // unlinked between the JS pass and this call: stale entry, not a miss
284
+ if (viewIt == views_.end()) continue;
285
+ auto& view = viewIt->second;
286
+
287
+ const std::string stateName = stateIt->second.asString();
288
+ auto props = entry.getDefault("props", nullptr);
289
+ if (props.isObject()) {
290
+ if (!view.stateProps.isObject()) {
291
+ view.stateProps = folly::dynamic::object();
292
+ }
293
+ view.stateProps[stateName] = std::move(props);
294
+ }
295
+ view.activeState = stateName;
296
+
297
+ buildViewUpdate(view, stateName, updates);
298
+ }
299
+
300
+ commitUpdates(rt, updates);
301
+ return jsi::Value::undefined();
302
+ }
303
+
304
+ jsi::Value HybridTamaguiRegistry::updateViewStateTables(
305
+ jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args,
306
+ size_t count) {
307
+ if (count < 1 || !args[0].isObject()) {
308
+ throw jsi::JSError(rt, "updateViewStateTables(entries) requires an array");
309
+ }
310
+ runtime_ = &rt;
311
+
312
+ auto entries = jsi::dynamicFromValue(rt, args[0]);
313
+ if (!entries.isArray()) {
314
+ throw jsi::JSError(rt, "updateViewStateTables(entries) requires an array");
315
+ }
316
+
317
+ LeafUpdates updates;
318
+ updates.reserve(entries.size());
319
+ for (auto& entry : entries) {
320
+ if (!entry.isObject()) continue;
321
+ auto idIt = entry.find("id");
322
+ auto stateIt = entry.find("state");
323
+ auto propsIt = entry.find("props");
324
+ if (idIt == entry.items().end() || stateIt == entry.items().end() ||
325
+ propsIt == entry.items().end() || !propsIt->second.isObject()) {
326
+ continue;
327
+ }
328
+
329
+ auto viewIt = views_.find(idIt->second.asDouble());
330
+ if (viewIt == views_.end()) continue;
331
+ auto& view = viewIt->second;
332
+ const std::string stateName = stateIt->second.asString();
333
+ if (!view.stateProps.isObject()) {
334
+ view.stateProps = folly::dynamic::object();
335
+ }
336
+ view.stateProps[stateName] = propsIt->second;
337
+
338
+ const auto* activeName = view.activeState.empty()
339
+ ? activeStateName(view.scopeId)
340
+ : &view.activeState;
341
+ if (activeName && *activeName == stateName) {
342
+ buildViewUpdate(view, stateName, updates);
343
+ }
344
+ }
345
+
346
+ commitUpdates(rt, updates);
347
+ return jsi::Value::undefined();
348
+ }
349
+
350
+ jsi::Value HybridTamaguiRegistry::getViewState(jsi::Runtime& rt,
351
+ const jsi::Value&,
352
+ const jsi::Value* args,
353
+ size_t count) {
354
+ if (count < 1 || !args[0].isNumber()) return jsi::Value::null();
355
+ auto it = views_.find(args[0].asNumber());
356
+ if (it == views_.end()) return jsi::Value::null();
357
+ const auto& view = it->second;
358
+
359
+ folly::dynamic out = folly::dynamic::object("scopeId", view.scopeId)(
360
+ "activeState", view.activeState)("base", view.baseProps)(
361
+ "states", view.stateProps);
362
+ const auto& family = view.node->getFamily();
363
+ out["nativeProps"] = family.nativeProps_DEPRECATED
364
+ ? *family.nativeProps_DEPRECATED
365
+ : folly::dynamic(nullptr);
366
+ return jsi::valueFromDynamic(rt, out);
367
+ }
368
+
369
+ } // namespace margelo::nitro::tamagui::registry