Skip to content

Commit

Permalink
feat: Set up hybridRef (#553)
Browse files Browse the repository at this point in the history
* feat: Create `HybridRef<T>`

* fix: Use `Methods`

* feat: Make `hybridRef` a function

* Try it

* locks

* feat: REF!!

* fix: Use JNISharedPtr for Android now
  • Loading branch information
mrousavy authored Feb 20, 2025
1 parent fa8ede8 commit 4111c67
Show file tree
Hide file tree
Showing 17 changed files with 167 additions and 37 deletions.
Binary file modified bun.lockb
Binary file not shown.
7 changes: 7 additions & 0 deletions example/src/screens/ViewScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ export function ViewScreenImpl() {
const colors = useColors()
const [counter, setCounter] = React.useState(0)
const [isUpdating, setIsUpdating] = React.useState(true)

const views = React.useMemo(
() =>
[...Array(counter)].map((_, i) => (
<TestView
key={i}
hybridRef={{
f: (ref) => {
console.log(`Ref initialized!`)
ref.someMethod()
},
}}
style={styles.view}
isBlue={i % 2 === 0}
someCallback={{ f: () => console.log(`Callback called!`) }}
Expand Down
16 changes: 16 additions & 0 deletions packages/nitrogen/src/syntax/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import path from 'path'
import type { SourceFile } from './SourceFile.js'
import type { Type } from './types/Type.js'
import { getTypeAs } from './types/getTypeAs.js'
import { OptionalType } from './types/OptionalType.js'

type Comment = '///' | '#'

Expand All @@ -18,6 +21,19 @@ ${comment}
`.trim()
}

export function isFunction(type: Type): boolean {
switch (type.kind) {
case 'function':
return true
case 'optional': {
const optional = getTypeAs(type, OptionalType)
return isFunction(optional.wrappingType)
}
default:
return false
}
}

export function toReferenceType(type: string): `const ${typeof type}&` {
return `const ${type}&`
}
Expand Down
33 changes: 25 additions & 8 deletions packages/nitrogen/src/views/CppHybridViewComponent.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import type { SourceFile } from '../syntax/SourceFile.js'
import type { HybridObjectSpec } from '../syntax/HybridObjectSpec.js'
import { createIndentation, indent } from '../utils.js'
import { createFileMetadataString, escapeCppName } from '../syntax/helpers.js'
import {
createFileMetadataString,
escapeCppName,
isFunction,
} from '../syntax/helpers.js'
import { NitroConfig } from '../config/NitroConfig.js'
import { getHybridObjectName } from '../syntax/getHybridObjectName.js'
import { includeHeader } from '../syntax/c++/includeNitroHeader.js'
import { createHostComponentJs } from './createHostComponentJs.js'
import { Property } from '../syntax/Property.js'
import { FunctionType } from '../syntax/types/FunctionType.js'
import { VoidType } from '../syntax/types/VoidType.js'
import { HybridObjectType } from '../syntax/types/HybridObjectType.js'
import { NamedWrappingType } from '../syntax/types/NamedWrappingType.js'
import { OptionalType } from '../syntax/types/OptionalType.js'

interface ViewComponentNames {
propsClassName: `${string}Props`
Expand All @@ -32,6 +42,14 @@ export function getViewComponentNames(
}
}

function getHybridRefProperty(spec: HybridObjectSpec): Property {
const hybrid = new HybridObjectType(spec)
const type = new FunctionType(new VoidType(), [
new NamedWrappingType('ref', hybrid),
])
return new Property('hybridRef', new OptionalType(type), false)
}

export function createViewComponentShadowNodeFiles(
spec: HybridObjectSpec
): SourceFile[] {
Expand All @@ -53,13 +71,12 @@ export function createViewComponentShadowNodeFiles(

const namespace = NitroConfig.getCxxNamespace('c++', 'views')

const properties = spec.properties.map(
const props = [...spec.properties, getHybridRefProperty(spec)]
const properties = props.map(
(p) => `CachedProp<${p.type.getCode('c++')}> ${escapeCppName(p.name)};`
)
const cases = spec.properties.map(
(p) => `case hashString("${p.name}"): return true;`
)
const includes = spec.properties.flatMap((p) =>
const cases = props.map((p) => `case hashString("${p.name}"): return true;`)
const includes = props.flatMap((p) =>
p.getRequiredImports().map((i) => includeHeader(i, true))
)

Expand Down Expand Up @@ -171,12 +188,12 @@ namespace ${namespace} {
'react::ViewProps(context, sourceProps, rawProps, filterObjectKeys)',
]
const propCopyInitializers = ['react::ViewProps()']
for (const prop of spec.properties) {
for (const prop of props) {
const name = escapeCppName(prop.name)
const type = prop.type.getCode('c++')

let valueConversion = `value`
if (prop.type.kind === 'function') {
if (isFunction(prop.type)) {
// Due to a React limitation, functions cannot be passed to native directly,
// because RN converts them to booleans (`true`). Nitro knows this and just
// wraps functions as objects - the original function is stored in `f`.
Expand Down
2 changes: 2 additions & 0 deletions packages/nitrogen/src/views/createHostComponentJs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { indent } from '../utils.js'

export function createHostComponentJs(spec: HybridObjectSpec): SourceFile[] {
const { T } = getHybridObjectName(spec.name)

const props = spec.properties.map((p) => `"${p.name}": true`)
props.push(`"hybridRef": true`)

const code = `
{
Expand Down
14 changes: 14 additions & 0 deletions packages/nitrogen/src/views/kotlin/KotlinHybridViewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class ${manager}: SimpleViewManager<View>() {
${stateUpdaterName}.updateViewProps(hybridView, stateWrapperImpl)
hybridView.afterUpdate()
// 3. Continue in base View props
return super.updateState(view, props, stateWrapper)
}
}
Expand Down Expand Up @@ -161,6 +162,7 @@ public:
return `
if (props.${name}.isDirty) {
view->${setter}(props.${name}.value);
// TODO: Set isDirty = false
}
`.trim()
})
Expand All @@ -170,6 +172,7 @@ ${createFileMetadataString(`J${stateUpdaterName}.cpp`)}
#include "J${stateUpdaterName}.hpp"
#include "views/${component}.hpp"
#include <NitroModules/NitroDefines.hpp>
#include <NitroModules/JNISharedPtr.hpp>
namespace ${cxxNamespace} {
Expand All @@ -190,6 +193,17 @@ void J${stateUpdaterName}::updateViewProps(jni::alias_ref<jni::JClass> /* class
}
const ${propsClassName}& props = maybeProps.value();
${indent(propsUpdaterCalls.join('\n'), ' ')}
// Update hybridRef if it changed
if (props.hybridRef.isDirty) {
// hybridRef changed - call it with new this
const auto& maybeFunc = props.hybridRef.value;
if (maybeFunc.has_value()) {
auto shared = JNISharedPtr::make_shared_from_jni<${JHybridTSpec}>(jni::make_global(javaView));
maybeFunc.value()(shared);
}
// TODO: Set isDirty = false
}
}
} // namespace ${cxxNamespace}
Expand Down
12 changes: 11 additions & 1 deletion packages/nitrogen/src/views/swift/SwiftHybridViewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,17 @@ using namespace ${namespace}::views;
swiftPart.afterUpdate();
// 3. Continue in base class
// 3. Update hybridRef if it changed
if (newViewProps.hybridRef.isDirty) {
// hybridRef changed - call it with new this
const auto& maybeFunc = newViewProps.hybridRef.value;
if (maybeFunc.has_value()) {
maybeFunc.value()(_hybridView);
}
newViewProps.hybridRef.isDirty = false;
}
// 4. Continue in base class
[super updateProps:props oldProps:oldProps];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "JHybridTestViewStateUpdater.hpp"
#include "views/HybridTestViewComponent.hpp"
#include <NitroModules/NitroDefines.hpp>
#include <NitroModules/JNISharedPtr.hpp>

namespace margelo::nitro::image::views {

Expand All @@ -29,9 +30,22 @@ void JHybridTestViewStateUpdater::updateViewProps(jni::alias_ref<jni::JClass> /*
const HybridTestViewProps& props = maybeProps.value();
if (props.isBlue.isDirty) {
view->setIsBlue(props.isBlue.value);
// TODO: Set isDirty = false
}
if (props.someCallback.isDirty) {
view->setSomeCallback(props.someCallback.value);
// TODO: Set isDirty = false
}

// Update hybridRef if it changed
if (props.hybridRef.isDirty) {
// hybridRef changed - call it with new this
const auto& maybeFunc = props.hybridRef.value;
if (maybeFunc.has_value()) {
auto shared = JNISharedPtr::make_shared_from_jni<JHybridTestViewSpec>(jni::make_global(javaView));
maybeFunc.value()(shared);
}
// TODO: Set isDirty = false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class HybridTestViewManager: SimpleViewManager<View>() {
HybridTestViewStateUpdater.updateViewProps(hybridView, stateWrapperImpl)
hybridView.afterUpdate()

// 3. Continue in base View props
return super.updateState(view, props, stateWrapper)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,17 @@ - (void) updateProps:(const react::Props::Shared&)props

swiftPart.afterUpdate();

// 3. Continue in base class
// 3. Update hybridRef if it changed
if (newViewProps.hybridRef.isDirty) {
// hybridRef changed - call it with new this
const auto& maybeFunc = newViewProps.hybridRef.value;
if (maybeFunc.has_value()) {
maybeFunc.value()(_hybridView);
}
newViewProps.hybridRef.isDirty = false;
}

// 4. Continue in base class
[super updateProps:props oldProps:oldProps];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,29 @@ namespace margelo::nitro::image::views {
} catch (const std::exception& exc) {
throw std::runtime_error(std::string("TestView.someCallback: ") + exc.what());
}
}()),
hybridRef([&]() -> CachedProp<std::optional<std::function<void(const std::shared_ptr<margelo::nitro::image::HybridTestViewSpec>& /* ref */)>>> {
try {
const react::RawValue* rawValue = rawProps.at("hybridRef", nullptr, nullptr);
if (rawValue == nullptr) return sourceProps.hybridRef;
const auto& [runtime, value] = (std::pair<jsi::Runtime*, jsi::Value>)*rawValue;
return CachedProp<std::optional<std::function<void(const std::shared_ptr<margelo::nitro::image::HybridTestViewSpec>& /* ref */)>>>::fromRawValue(*runtime, value.asObject(*runtime).getProperty(*runtime, "f"), sourceProps.hybridRef);
} catch (const std::exception& exc) {
throw std::runtime_error(std::string("TestView.hybridRef: ") + exc.what());
}
}()) { }

HybridTestViewProps::HybridTestViewProps(const HybridTestViewProps& other):
react::ViewProps(),
isBlue(other.isBlue),
someCallback(other.someCallback) { }
someCallback(other.someCallback),
hybridRef(other.hybridRef) { }

bool HybridTestViewProps::filterObjectKeys(const std::string& propName) {
switch (hashString(propName)) {
case hashString("isBlue"): return true;
case hashString("someCallback"): return true;
case hashString("hybridRef"): return true;
default: return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
#include <react/renderer/components/view/ViewProps.h>

#include <functional>
#include <optional>
#include <functional>
#include <memory>
#include "HybridTestViewSpec.hpp"

namespace margelo::nitro::image::views {

Expand All @@ -41,6 +45,7 @@ namespace margelo::nitro::image::views {
public:
CachedProp<bool> isBlue;
CachedProp<std::function<void()>> someCallback;
CachedProp<std::optional<std::function<void(const std::shared_ptr<margelo::nitro::image::HybridTestViewSpec>& /* ref */)>>> hybridRef;

private:
static bool filterObjectKeys(const std::string& propName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"directEventTypes": {},
"validAttributes": {
"isBlue": true,
"someCallback": true
"someCallback": true,
"hybridRef": true
}
}
4 changes: 3 additions & 1 deletion packages/react-native-nitro-image/src/views/TestView.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getHostComponent } from 'react-native-nitro-modules'
import { getHostComponent, type HybridRef } from 'react-native-nitro-modules'
import TestViewConfig from '../../nitrogen/generated/shared/json/TestViewConfig.json'
import {
type TestViewMethods,
Expand All @@ -12,3 +12,5 @@ export const TestView = getHostComponent<TestViewProps, TestViewMethods>(
'TestView',
() => TestViewConfig
)

export type TestViewRef = HybridRef<TestViewProps, TestViewMethods>
2 changes: 2 additions & 0 deletions packages/react-native-nitro-modules/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ export * from './HybridObject'
export * from './NitroModules'
export * from './AnyMap'
export * from './Constructor'

export * from './views/HybridView'
export * from './views/getHostComponent'
Loading

0 comments on commit 4111c67

Please sign in to comment.