diff --git a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt index b3953629..d4f2ba4a 100644 --- a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -90,6 +90,10 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { override var fit: Fit? = null override var layoutScaleFactor: Double? = null + // Accepted for API parity; frame-rate control is only implemented on the + // experimental backends. + override var frameRate: Variant_Double_FrameRateRange? = null + // Accepted for API parity; semantics are only available in the new Rive // runtime, and Android support is pending upstream (iOS-only for now). override var semantics: Semantics? = null diff --git a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt index 7cc08ce6..4d18af5d 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -97,6 +97,17 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { override var fit: Fit? = null override var layoutScaleFactor: Double? = null + // The render loop can only skip frames, so a range is honored best-effort + // as a cap at preferred ?? maximum. + override var frameRate: Variant_Double_FrameRateRange? = null + set(value) { + field = value + view.frameRate = value?.match( + first = { it }, + second = { range -> range.preferred ?: range.maximum } + ) + } + // Accepted for API parity; semantics support is pending in the upstream // rive-android runtime (iOS-only for now). override var semantics: Semantics? = null diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index 3e14c561..dff96577 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -2,11 +2,14 @@ package com.rive import android.annotation.SuppressLint import android.graphics.SurfaceTexture +import android.os.Build import android.util.Log import android.view.Choreographer import android.view.MotionEvent import android.view.TextureView +import android.view.View import android.widget.FrameLayout +import com.facebook.react.bridge.UiThreadUtil import app.rive.Artboard import app.rive.Fit import app.rive.RiveFile @@ -53,8 +56,20 @@ data class ViewConfiguration( class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { companion object { private const val TAG = "RiveReactNativeView" + + // Half of a 120Hz vsync: lets a cap land on the nearest vsync multiple + // (e.g. every 4th frame at 120Hz for a 30fps cap) instead of drifting past + // it and halving the effective rate. + private const val CAP_TOLERANCE_NS = 4_000_000L } + // Render at most this many frames per second; null = every vsync. + var frameRate: Double? = null + set(value) { + field = value + updateFrameRateHint() + } + var onError: ((String) -> Unit)? = null private val errorListener: (String) -> Unit = { msg -> @@ -85,6 +100,11 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { private var lastFrameTimeNs = 0L private var frameCount = 0L + // While paused the loop still ticks, but only draws when something changed + // (initial content, resize, rebinding) — otherwise a paused view would keep + // re-rendering identical frames at full refresh rate. + private var needsRedraw = true + @Volatile private var paused = false @@ -118,6 +138,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, w: Int, h: Int) { this@RiveReactNativeView.surfaceWidth = w this@RiveReactNativeView.surfaceHeight = h + this@RiveReactNativeView.needsRedraw = true // Since 11.7.x the render target keeps its creation-time size and // RiveSurface.resize() is internal to the SDK, so only the artboard // is resized here (same behavior as before the 11.7.2 bump). @@ -136,6 +157,26 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { override fun doFrame(frameTimeNanos: Long) { if (!renderLoopRunning || disposed) return + if (paused && !needsRedraw) { + // Keep the timebase fresh so resuming advances by one frame, not by + // the whole pause span. + lastFrameTimeNs = frameTimeNanos + Choreographer.getInstance().postFrameCallback(this) + return + } + + val capPeriodNs = frameRate + ?.takeIf { it > 0 } + ?.let { (1_000_000_000.0 / it).toLong() } + if (capPeriodNs != null && lastFrameTimeNs != 0L && + frameTimeNanos - lastFrameTimeNs < capPeriodNs - CAP_TOLERANCE_NS + ) { + // Skip without touching lastFrameTimeNs: the eventual advance must + // cover the full elapsed time so capped playback keeps wall-clock speed. + Choreographer.getInstance().postFrameCallback(this) + return + } + val deltaTime = if (lastFrameTimeNs == 0L) { Duration.ZERO } else { @@ -154,6 +195,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { worker.advanceStateMachine(sm, deltaTime) } worker.draw(art, sm, rs, activeFit) + needsRedraw = false frameCount++ val isFirstFrame = frameCount == 1L if (isFirstFrame) { @@ -174,14 +216,29 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { if (renderLoopRunning) return renderLoopRunning = true lastFrameTimeNs = 0L + updateFrameRateHint() Choreographer.getInstance().postFrameCallback(renderCallback) } private fun stopRenderLoop() { renderLoopRunning = false + updateFrameRateHint() Choreographer.getInstance().removeFrameCallback(renderCallback) } + // Advisory platform hint (upstream applies the same one inside its Compose + // loop): on capable displays a capped, actively-drawing view lets the + // system lower the refresh rate, saving power beyond the skipped draws. + // Callers may be off-main (play()/pause() run on a coroutine), so hop. + private fun updateFrameRateHint() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return + UiThreadUtil.runOnUiThread { + val fps = frameRate?.takeIf { it > 0 && renderLoopRunning && !paused } + textureView.requestedFrameRate = + fps?.toFloat() ?: View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE + } + } + suspend fun awaitViewReady(): Boolean { return viewReadyDeferred.await() } @@ -189,6 +246,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { fun configure(config: ViewConfiguration, dataBindingChanged: Boolean, reload: Boolean = false, initialUpdate: Boolean = false) { riveWorker = config.riveWorker activeFit = config.fit + needsRedraw = true Log.d( TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=$surfaceWidth surfaceH=$surfaceHeight" @@ -371,6 +429,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { val smHandle = stateMachineHandle if (worker != null && smHandle != null) { worker.bindViewModelInstance(smHandle, instance.instanceHandle) + needsRedraw = true } else { Log.w(TAG, "Cannot bind VMI: worker or state machine handle not available") } @@ -378,10 +437,12 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { fun play() { paused = false + updateFrameRateHint() } fun pause() { paused = true + updateFrameRateHint() } // Deprecated: the experimental Rive runtime has no reset primitive. @@ -391,6 +452,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { fun playIfNeeded() { paused = false + updateFrameRateHint() } fun setNumberInputValue(name: String, value: Double, path: String?) { diff --git a/example/src/reproducers/FrameRateCap.tsx b/example/src/reproducers/FrameRateCap.tsx new file mode 100644 index 00000000..6de1cce8 --- /dev/null +++ b/example/src/reproducers/FrameRateCap.tsx @@ -0,0 +1,183 @@ +import { useRef, useState } from 'react'; +import { View, Text, StyleSheet, Pressable } from 'react-native'; +import { + RiveView, + useRiveFile, + Fit, + type FrameRateRange, + type RiveViewRef, +} from '@rive-app/react-native'; +import { type Metadata } from '../shared/metadata'; + +/** + * Manual verifier for the frameRate prop (issue #332). + * + * A looping animation renders at the display refresh rate by default; picking + * a cap should make it visibly step at that rate while keeping wall-clock + * playback speed (the loop takes the same time to complete). Pause should + * stop per-frame rendering entirely. + * + * Only the experimental (default) backends honor the prop. + */ + +type FrameRateOption = { + label: string; + value: number | FrameRateRange | undefined; +}; + +const OPTIONS: FrameRateOption[] = [ + { label: 'Uncapped', value: undefined }, + { label: '30 fps', value: 30 }, + { label: '15 fps', value: 15 }, + { label: '5 fps', value: 5 }, + { + label: 'Range 10–20 (pref 15)', + value: { minimum: 10, maximum: 20, preferred: 15 }, + }, +]; + +export default function FrameRateCap() { + const [optionIndex, setOptionIndex] = useState(0); + const [paused, setPaused] = useState(false); + const [mounted, setMounted] = useState(true); + const viewRef = useRef(null); + const { riveFile } = useRiveFile(require('../../assets/rive/rewards.riv')); + + const selected = OPTIONS[optionIndex]!; + + const togglePaused = () => { + if (paused) { + viewRef.current?.play(); + } else { + viewRef.current?.pause(); + } + setPaused(!paused); + }; + + return ( + + Frame Rate Cap + + Lower caps should visibly step but finish the loop in the same time. + + + {OPTIONS.map((option, index) => ( + setOptionIndex(index)} + > + + {option.label} + + + ))} + + {paused ? 'Play' : 'Pause'} + + { + setPaused(false); + setMounted((m) => !m); + }} + > + {mounted ? 'Unmount' : 'Mount'} + + + + {mounted && riveFile ? ( + (viewRef.current = ref) }} + style={styles.rive} + /> + ) : ( + + {mounted ? 'Loading…' : 'Unmounted'} + + )} + + + ); +} + +FrameRateCap.metadata = { + name: 'Frame Rate Cap', + description: + 'frameRate prop: cap the render loop fps (experimental backends only)', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#fff', + }, + title: { + fontSize: 20, + fontWeight: 'bold', + textAlign: 'center', + }, + subtitle: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginBottom: 16, + }, + buttonRow: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 8, + marginBottom: 16, + }, + button: { + paddingHorizontal: 14, + paddingVertical: 10, + backgroundColor: '#eee', + borderRadius: 8, + }, + buttonActive: { + backgroundColor: '#007AFF', + }, + buttonText: { + color: '#333', + fontWeight: '600', + fontSize: 13, + }, + buttonTextActive: { + color: '#fff', + }, + riveContainer: { + flex: 1, + backgroundColor: '#f5f5f5', + borderRadius: 8, + overflow: 'hidden', + justifyContent: 'center', + }, + rive: { + flex: 1, + }, + loading: { + textAlign: 'center', + color: '#666', + }, +}); diff --git a/ios/legacy/HybridRiveView.swift b/ios/legacy/HybridRiveView.swift index 8ae0a9bc..de8f80d5 100644 --- a/ios/legacy/HybridRiveView.swift +++ b/ios/legacy/HybridRiveView.swift @@ -106,6 +106,9 @@ class HybridRiveView: HybridRiveViewSpec { var alignment: Alignment? var fit: Fit? var layoutScaleFactor: Double? + // Accepted for API parity; frame-rate control is only implemented on the + // experimental backends. + var frameRate: Variant_Double_FrameRateRange? // Accepted for API parity; semantics are only available in the new Rive // runtime (the experimental backend). var semantics: Semantics? diff --git a/ios/new/HybridRiveView.swift b/ios/new/HybridRiveView.swift index b63496e5..7db3509e 100644 --- a/ios/new/HybridRiveView.swift +++ b/ios/new/HybridRiveView.swift @@ -98,6 +98,7 @@ class HybridRiveView: HybridRiveViewSpec { var alignment: Alignment? var fit: Fit? var layoutScaleFactor: Double? + var frameRate: Variant_Double_FrameRateRange? var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } @@ -213,6 +214,7 @@ class HybridRiveView: HybridRiveViewSpec { file: riveFile, fit: toRiveFit(fit, alignment: alignment, layoutScaleFactor: layoutScaleFactor), semantics: toRiveSemantics(semantics), + frameRate: toRiveFrameRate(frameRate), bindData: try dataBind.toBindData() ) @@ -260,6 +262,21 @@ class HybridRiveView: HybridRiveViewSpec { } } + private func toRiveFrameRate(_ frameRate: Variant_Double_FrameRateRange?) -> RiveRuntime.FrameRate { + switch frameRate { + case .first(let fps): + return .fps(Int(fps.rounded())) + case .second(let range): + return .range( + minimum: Float(range.minimum), + maximum: Float(range.maximum), + preferred: range.preferred.map { Float($0) } + ) + case nil: + return RiveUIView.Constants.Defaults.frameRate + } + } + private func toRiveSemantics(_ semantics: Semantics?) -> RiveRuntime.Semantics { switch semantics { case .off: return .off diff --git a/ios/new/RiveReactNativeView.swift b/ios/new/RiveReactNativeView.swift index da1fc47b..d79086a3 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -16,6 +16,7 @@ struct ViewConfiguration { let file: File let fit: RiveRuntime.Fit let semantics: RiveRuntime.Semantics + let frameRate: RiveRuntime.FrameRate let bindData: BindData } @@ -31,6 +32,9 @@ class RiveReactNativeView: UIView { private var semantics: RiveRuntime.Semantics = RiveUIView.Constants.Defaults.semantics { didSet { riveUIView?.semantics = semantics } } + private var frameRate: RiveRuntime.FrameRate = RiveUIView.Constants.Defaults.frameRate { + didSet { riveUIView?.frameRate = frameRate } + } var autoPlay: Bool = true /// Configure failures are reported here (wired to the onError prop). @@ -57,6 +61,7 @@ class RiveReactNativeView: UIView { dispatchPrecondition(condition: .onQueue(.main)) semantics = config.semantics + frameRate = config.frameRate if reload { cleanup() @@ -215,9 +220,11 @@ class RiveReactNativeView: UIView { existing.rive = rive existing.isPaused = isPaused existing.semantics = semantics + existing.frameRate = frameRate } else { let uiView = RiveUIView(rive: rive, isPaused: isPaused) uiView.semantics = semantics + uiView.frameRate = frameRate uiView.translatesAutoresizingMaskIntoConstraints = false addSubview(uiView) NSLayoutConstraint.activate([ diff --git a/nitrogen/generated/android/c++/JFrameRateRange.hpp b/nitrogen/generated/android/c++/JFrameRateRange.hpp new file mode 100644 index 00000000..ca5ecaf4 --- /dev/null +++ b/nitrogen/generated/android/c++/JFrameRateRange.hpp @@ -0,0 +1,65 @@ +/// +/// JFrameRateRange.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "FrameRateRange.hpp" + +#include + +namespace margelo::nitro::rive { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "FrameRateRange" and the the Kotlin data class "FrameRateRange". + */ + struct JFrameRateRange final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/FrameRateRange;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct FrameRateRange by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + FrameRateRange toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldMinimum = clazz->getField("minimum"); + double minimum = this->getFieldValue(fieldMinimum); + static const auto fieldMaximum = clazz->getField("maximum"); + double maximum = this->getFieldValue(fieldMaximum); + static const auto fieldPreferred = clazz->getField("preferred"); + jni::local_ref preferred = this->getFieldValue(fieldPreferred); + return FrameRateRange( + minimum, + maximum, + preferred != nullptr ? std::make_optional(preferred->value()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const FrameRateRange& value) { + using JSignature = JFrameRateRange(double, double, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + value.minimum, + value.maximum, + value.preferred.has_value() ? jni::JDouble::valueOf(value.preferred.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp index 28b00f64..b6827bc7 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp @@ -13,6 +13,8 @@ namespace margelo::nitro::rive { class HybridRiveFileSpec; } namespace margelo::nitro::rive { enum class Alignment; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `FrameRateRange` to properly resolve imports. +namespace margelo::nitro::rive { struct FrameRateRange; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -39,12 +41,15 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "JAlignment.hpp" #include "Fit.hpp" #include "JFit.hpp" +#include "FrameRateRange.hpp" +#include +#include "JVariant_Double_FrameRateRange.hpp" +#include "JFrameRateRange.hpp" #include "Semantics.hpp" #include "JSemantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" -#include #include "JVariant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.hpp" #include "JHybridViewModelInstanceSpec.hpp" #include "JDataBindMode.hpp" @@ -160,6 +165,15 @@ namespace margelo::nitro::rive { static const auto method = _javaPart->javaClassStatic()->getMethod /* layoutScaleFactor */)>("setLayoutScaleFactor"); method(_javaPart, layoutScaleFactor.has_value() ? jni::JDouble::valueOf(layoutScaleFactor.value()) : nullptr); } + std::optional> JHybridRiveViewSpec::getFrameRate() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("getFrameRate"); + auto __result = method(_javaPart); + return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt; + } + void JHybridRiveViewSpec::setFrameRate(const std::optional>& frameRate) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* frameRate */)>("setFrameRate"); + method(_javaPart, frameRate.has_value() ? JVariant_Double_FrameRateRange::fromCpp(frameRate.value()) : nullptr); + } std::optional JHybridRiveViewSpec::getSemantics() { static const auto method = _javaPart->javaClassStatic()->getMethod()>("getSemantics"); auto __result = method(_javaPart); diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp index 89190c98..fc8641bc 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp @@ -64,6 +64,8 @@ namespace margelo::nitro::rive { void setFit(std::optional fit) override; std::optional getLayoutScaleFactor() override; void setLayoutScaleFactor(std::optional layoutScaleFactor) override; + std::optional> getFrameRate() override; + void setFrameRate(const std::optional>& frameRate) override; std::optional getSemantics() override; void setSemantics(std::optional semantics) override; std::optional, DataBindMode, DataBindByName>> getDataBind() override; diff --git a/nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.cpp b/nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.cpp new file mode 100644 index 00000000..b2a76aaa --- /dev/null +++ b/nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.cpp @@ -0,0 +1,26 @@ +/// +/// JVariant_Double_FrameRateRange.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "JVariant_Double_FrameRateRange.hpp" + +namespace margelo::nitro::rive { + /** + * Converts JVariant_Double_FrameRateRange to std::variant + */ + std::variant JVariant_Double_FrameRateRange::toCpp() const { + if (isInstanceOf(JVariant_Double_FrameRateRange_impl::First::javaClassStatic())) { + // It's a `double` + auto jniValue = static_cast(this)->getValue(); + return jniValue; + } else if (isInstanceOf(JVariant_Double_FrameRateRange_impl::Second::javaClassStatic())) { + // It's a `FrameRateRange` + auto jniValue = static_cast(this)->getValue(); + return jniValue->toCpp(); + } + throw std::invalid_argument("Variant is unknown Kotlin instance!"); + } +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.hpp b/nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.hpp new file mode 100644 index 00000000..df82fbd9 --- /dev/null +++ b/nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.hpp @@ -0,0 +1,70 @@ +/// +/// JVariant_Double_FrameRateRange.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include "FrameRateRange.hpp" +#include +#include "JFrameRateRange.hpp" +#include + +namespace margelo::nitro::rive { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ std::variant and the Java class "Variant_Double_FrameRateRange". + */ + class JVariant_Double_FrameRateRange: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Double_FrameRateRange;"; + + static jni::local_ref create_0(double value) { + static const auto method = javaClassStatic()->getStaticMethod("create"); + return method(javaClassStatic(), value); + } + static jni::local_ref create_1(jni::alias_ref value) { + static const auto method = javaClassStatic()->getStaticMethod)>("create"); + return method(javaClassStatic(), value); + } + + static jni::local_ref fromCpp(const std::variant& variant) { + switch (variant.index()) { + case 0: return create_0(std::get<0>(variant)); + case 1: return create_1(JFrameRateRange::fromCpp(std::get<1>(variant))); + default: throw std::invalid_argument("Variant holds unknown index! (" + std::to_string(variant.index()) + ")"); + } + } + + [[nodiscard]] std::variant toCpp() const; + }; + + namespace JVariant_Double_FrameRateRange_impl { + class First final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Double_FrameRateRange$First;"; + + [[nodiscard]] double getValue() const { + static const auto field = javaClassStatic()->getField("value"); + return getFieldValue(field); + } + }; + + class Second final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Double_FrameRateRange$Second;"; + + [[nodiscard]] jni::local_ref getValue() const { + static const auto field = javaClassStatic()->getField("value"); + return getFieldValue(field); + } + }; + } // namespace JVariant_Double_FrameRateRange_impl +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp index 4bef850c..e003051d 100644 --- a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp +++ b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp @@ -65,6 +65,10 @@ void JHybridRiveViewStateUpdater::updateViewProps(jni::alias_ref /* hybridView->setLayoutScaleFactor(props->layoutScaleFactor.value); props->layoutScaleFactor.isDirty = false; } + if (props->frameRate.isDirty) { + hybridView->setFrameRate(props->frameRate.value); + props->frameRate.isDirty = false; + } if (props->semantics.isDirty) { hybridView->setSemantics(props->semantics.value); props->semantics.isDirty = false; diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/FrameRateRange.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/FrameRateRange.kt new file mode 100644 index 00000000..2c821807 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/FrameRateRange.kt @@ -0,0 +1,44 @@ +/// +/// FrameRateRange.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.rive + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "FrameRateRange". + */ +@DoNotStrip +@Keep +data class FrameRateRange( + @DoNotStrip + @Keep + val minimum: Double, + @DoNotStrip + @Keep + val maximum: Double, + @DoNotStrip + @Keep + val preferred: Double? +) { + /* primary constructor */ + + companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(minimum: Double, maximum: Double, preferred: Double?): FrameRateRange { + return FrameRateRange(minimum, maximum, preferred) + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt index 43700d52..be540182 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt @@ -69,6 +69,12 @@ abstract class HybridRiveViewSpec: HybridView() { @set:Keep abstract var layoutScaleFactor: Double? + @get:DoNotStrip + @get:Keep + @set:DoNotStrip + @set:Keep + abstract var frameRate: Variant_Double_FrameRateRange? + @get:DoNotStrip @get:Keep @set:DoNotStrip diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Double_FrameRateRange.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Double_FrameRateRange.kt new file mode 100644 index 00000000..3884b6f6 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Double_FrameRateRange.kt @@ -0,0 +1,53 @@ +/// +/// Variant_Double_FrameRateRange.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.rive + +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the TypeScript variant "Double | FrameRateRange". + */ +@Suppress("ClassName") +@DoNotStrip +sealed class Variant_Double_FrameRateRange { + @DoNotStrip + data class First(@DoNotStrip val value: Double): Variant_Double_FrameRateRange() + @DoNotStrip + data class Second(@DoNotStrip val value: FrameRateRange): Variant_Double_FrameRateRange() + + val isFirst: Boolean + get() = this is First + val isSecond: Boolean + get() = this is Second + + fun asFirstOrNull(): Double? { + val value = (this as? First)?.value ?: return null + return value + } + fun asSecondOrNull(): FrameRateRange? { + val value = (this as? Second)?.value ?: return null + return value + } + + inline fun match(first: (Double) -> R, second: (FrameRateRange) -> R): R { + return when (this) { + is First -> first(value) + is Second -> second(value) + } + } + + companion object { + @JvmStatic + @DoNotStrip + fun create(value: Double): Variant_Double_FrameRateRange = First(value) + @JvmStatic + @DoNotStrip + fun create(value: FrameRateRange): Variant_Double_FrameRateRange = Second(value) + } +} diff --git a/nitrogen/generated/android/rive+autolinking.cmake b/nitrogen/generated/android/rive+autolinking.cmake index ffe01178..31833465 100644 --- a/nitrogen/generated/android/rive+autolinking.cmake +++ b/nitrogen/generated/android/rive+autolinking.cmake @@ -67,6 +67,7 @@ target_sources( ../nitrogen/generated/android/c++/JHybridRiveLoggerSpec.cpp ../nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.cpp ../nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp + ../nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.cpp ../nitrogen/generated/android/c++/JVariant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.cpp ../nitrogen/generated/android/c++/JEventPropertiesOutput.cpp ../nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp diff --git a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp index 792ec88d..f44c2c59 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp @@ -20,6 +20,8 @@ namespace margelo::nitro::rive { struct DataBindByName; } namespace margelo::nitro::rive { enum class DataBindMode; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `FrameRateRange` to properly resolve imports. +namespace margelo::nitro::rive { struct FrameRateRange; } // Forward declaration of `HybridBindableArtboardSpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridBindableArtboardSpec; } // Forward declaration of `HybridFallbackFontSpec` to properly resolve imports. @@ -140,6 +142,7 @@ namespace RNRive { class HybridViewModelTriggerPropertySpec_cxx; } #include "DataBindByName.hpp" #include "DataBindMode.hpp" #include "Fit.hpp" +#include "FrameRateRange.hpp" #include "HybridBindableArtboardSpec.hpp" #include "HybridFallbackFontSpec.hpp" #include "HybridRiveFileFactorySpec.hpp" @@ -958,6 +961,50 @@ namespace margelo::nitro::rive::bridge::swift { return optional.value(); } + // pragma MARK: std::variant + /** + * Wrapper struct for `std::variant`. + * std::variant cannot be used in Swift because of a Swift bug. + * Not even specializing it works. So we create a wrapper struct. + */ + struct std__variant_double__FrameRateRange_ final { + std::variant variant; + std__variant_double__FrameRateRange_(std::variant variant): variant(variant) { } + operator std::variant() const noexcept { + return variant; + } + inline size_t index() const noexcept { + return variant.index(); + } + inline double get_0() const noexcept { + return std::get<0>(variant); + } + inline FrameRateRange get_1() const noexcept { + return std::get<1>(variant); + } + }; + inline std__variant_double__FrameRateRange_ create_std__variant_double__FrameRateRange_(double value) noexcept { + return std__variant_double__FrameRateRange_(value); + } + inline std__variant_double__FrameRateRange_ create_std__variant_double__FrameRateRange_(const FrameRateRange& value) noexcept { + return std__variant_double__FrameRateRange_(value); + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__variant_double__FrameRateRange__ = std::optional>; + inline std::optional> create_std__optional_std__variant_double__FrameRateRange__(const std::variant& value) noexcept { + return std::optional>(value); + } + inline bool has_value_std__optional_std__variant_double__FrameRateRange__(const std::optional>& optional) noexcept { + return optional.has_value(); + } + inline std::variant get_std__optional_std__variant_double__FrameRateRange__(const std::optional>& optional) noexcept { + return optional.value(); + } + // pragma MARK: std::optional /** * Specialized version of `std::optional`. diff --git a/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp index 02505b5e..08c77231 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp @@ -22,6 +22,8 @@ namespace margelo::nitro::rive { struct DataBindByName; } namespace margelo::nitro::rive { enum class DataBindMode; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `FrameRateRange` to properly resolve imports. +namespace margelo::nitro::rive { struct FrameRateRange; } // Forward declaration of `HybridBindableArtboardSpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridBindableArtboardSpec; } // Forward declaration of `HybridFallbackFontSpec` to properly resolve imports. @@ -97,6 +99,7 @@ namespace margelo::nitro::rive { enum class ViewModelPropertyType; } #include "DataBindByName.hpp" #include "DataBindMode.hpp" #include "Fit.hpp" +#include "FrameRateRange.hpp" #include "HybridBindableArtboardSpec.hpp" #include "HybridFallbackFontSpec.hpp" #include "HybridRiveFileFactorySpec.hpp" diff --git a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp index e2119b22..b3c0dfe7 100644 --- a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp @@ -18,6 +18,8 @@ namespace margelo::nitro::rive { class HybridRiveFileSpec; } namespace margelo::nitro::rive { enum class Alignment; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `FrameRateRange` to properly resolve imports. +namespace margelo::nitro::rive { struct FrameRateRange; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -41,11 +43,12 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "HybridRiveFileSpec.hpp" #include "Alignment.hpp" #include "Fit.hpp" +#include "FrameRateRange.hpp" +#include #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" -#include #include "RiveError.hpp" #include #include "RiveErrorType.hpp" @@ -149,6 +152,13 @@ namespace margelo::nitro::rive { inline void setLayoutScaleFactor(std::optional layoutScaleFactor) noexcept override { _swiftPart.setLayoutScaleFactor(layoutScaleFactor); } + inline std::optional> getFrameRate() noexcept override { + auto __result = _swiftPart.getFrameRate(); + return __result; + } + inline void setFrameRate(const std::optional>& frameRate) noexcept override { + _swiftPart.setFrameRate(frameRate); + } inline std::optional getSemantics() noexcept override { auto __result = _swiftPart.getSemantics(); return __result; diff --git a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm index d449bd22..f1669805 100644 --- a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm +++ b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm @@ -107,6 +107,11 @@ - (void) updateProps:(const std::shared_ptr&)props swiftPart.setLayoutScaleFactor(newViewProps.layoutScaleFactor.value); newViewProps.layoutScaleFactor.isDirty = false; } + // frameRate: optional + if (newViewProps.frameRate.isDirty) { + swiftPart.setFrameRate(newViewProps.frameRate.value); + newViewProps.frameRate.isDirty = false; + } // semantics: optional if (newViewProps.semantics.isDirty) { swiftPart.setSemantics(newViewProps.semantics.value); diff --git a/nitrogen/generated/ios/swift/FrameRateRange.swift b/nitrogen/generated/ios/swift/FrameRateRange.swift new file mode 100644 index 00000000..32205038 --- /dev/null +++ b/nitrogen/generated/ios/swift/FrameRateRange.swift @@ -0,0 +1,52 @@ +/// +/// FrameRateRange.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `FrameRateRange`, backed by a C++ struct. + */ +public typealias FrameRateRange = margelo.nitro.rive.FrameRateRange + +public extension FrameRateRange { + private typealias bridge = margelo.nitro.rive.bridge.swift + + /** + * Create a new instance of `FrameRateRange`. + */ + init(minimum: Double, maximum: Double, preferred: Double?) { + self.init(minimum, maximum, { () -> bridge.std__optional_double_ in + if let __unwrappedValue = preferred { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + @inline(__always) + var minimum: Double { + return self.__minimum + } + + @inline(__always) + var maximum: Double { + return self.__maximum + } + + @inline(__always) + var preferred: Double? { + return { () -> Double? in + if bridge.has_value_std__optional_double_(self.__preferred) { + let __unwrapped = bridge.get_std__optional_double_(self.__preferred) + return __unwrapped + } else { + return nil + } + }() + } +} diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift index e217f1a2..d2e38de6 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift @@ -17,6 +17,7 @@ public protocol HybridRiveViewSpec_protocol: HybridObject, HybridView { var alignment: Alignment? { get set } var fit: Fit? { get set } var layoutScaleFactor: Double? { get set } + var frameRate: Variant_Double_FrameRateRange? { get set } var semantics: Semantics? { get set } var dataBind: Variant__any_HybridViewModelInstanceSpec__DataBindMode_DataBindByName? { get set } var onError: (_ error: RiveError) -> Void { get set } diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift index 95e15c7f..bb9ead71 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift @@ -269,6 +269,49 @@ open class HybridRiveViewSpec_cxx { } } + public final var frameRate: bridge.std__optional_std__variant_double__FrameRateRange__ { + @inline(__always) + get { + return { () -> bridge.std__optional_std__variant_double__FrameRateRange__ in + if let __unwrappedValue = self.__implementation.frameRate { + return bridge.create_std__optional_std__variant_double__FrameRateRange__({ () -> bridge.std__variant_double__FrameRateRange_ in + switch __unwrappedValue { + case .first(let __value): + return bridge.create_std__variant_double__FrameRateRange_(__value) + case .second(let __value): + return bridge.create_std__variant_double__FrameRateRange_(__value) + } + }().variant) + } else { + return .init() + } + }() + } + @inline(__always) + set { + self.__implementation.frameRate = { () -> Variant_Double_FrameRateRange? in + if bridge.has_value_std__optional_std__variant_double__FrameRateRange__(newValue) { + let __unwrapped = bridge.get_std__optional_std__variant_double__FrameRateRange__(newValue) + return { () -> Variant_Double_FrameRateRange in + let __variant = bridge.std__variant_double__FrameRateRange_(__unwrapped) + switch __variant.index() { + case 0: + let __actual = __variant.get_0() + return .first(__actual) + case 1: + let __actual = __variant.get_1() + return .second(__actual) + default: + fatalError("Variant can never have index \(__variant.index())!") + } + }() + } else { + return nil + } + }() + } + } + public final var semantics: bridge.std__optional_Semantics_ { @inline(__always) get { diff --git a/nitrogen/generated/ios/swift/Variant_Double_FrameRateRange.swift b/nitrogen/generated/ios/swift/Variant_Double_FrameRateRange.swift new file mode 100644 index 00000000..0af12d68 --- /dev/null +++ b/nitrogen/generated/ios/swift/Variant_Double_FrameRateRange.swift @@ -0,0 +1,18 @@ +/// +/// Variant_Double_FrameRateRange.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + + + +/** + * An Swift enum with associated values representing a Variant/Union type. + * JS type: `number | struct` + */ +@frozen +public indirect enum Variant_Double_FrameRateRange { + case first(Double) + case second(FrameRateRange) +} diff --git a/nitrogen/generated/shared/c++/FrameRateRange.hpp b/nitrogen/generated/shared/c++/FrameRateRange.hpp new file mode 100644 index 00000000..eef7b17a --- /dev/null +++ b/nitrogen/generated/shared/c++/FrameRateRange.hpp @@ -0,0 +1,91 @@ +/// +/// FrameRateRange.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::rive { + + /** + * A struct which can be represented as a JavaScript object (FrameRateRange). + */ + struct FrameRateRange final { + public: + double minimum SWIFT_PRIVATE; + double maximum SWIFT_PRIVATE; + std::optional preferred SWIFT_PRIVATE; + + public: + FrameRateRange() = default; + explicit FrameRateRange(double minimum, double maximum, std::optional preferred): minimum(minimum), maximum(maximum), preferred(preferred) {} + + public: + friend bool operator==(const FrameRateRange& lhs, const FrameRateRange& rhs) = default; + }; + +} // namespace margelo::nitro::rive + +namespace margelo::nitro { + + // C++ FrameRateRange <> JS FrameRateRange (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::rive::FrameRateRange fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::rive::FrameRateRange( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "minimum"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maximum"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "preferred"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::rive::FrameRateRange& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "minimum"), JSIConverter::toJSI(runtime, arg.minimum)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "maximum"), JSIConverter::toJSI(runtime, arg.maximum)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "preferred"), JSIConverter>::toJSI(runtime, arg.preferred)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "minimum")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maximum")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "preferred")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp index 2ef5d7ba..dadb86eb 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp @@ -28,6 +28,8 @@ namespace margelo::nitro::rive { prototype.registerHybridSetter("fit", &HybridRiveViewSpec::setFit); prototype.registerHybridGetter("layoutScaleFactor", &HybridRiveViewSpec::getLayoutScaleFactor); prototype.registerHybridSetter("layoutScaleFactor", &HybridRiveViewSpec::setLayoutScaleFactor); + prototype.registerHybridGetter("frameRate", &HybridRiveViewSpec::getFrameRate); + prototype.registerHybridSetter("frameRate", &HybridRiveViewSpec::setFrameRate); prototype.registerHybridGetter("semantics", &HybridRiveViewSpec::getSemantics); prototype.registerHybridSetter("semantics", &HybridRiveViewSpec::setSemantics); prototype.registerHybridGetter("dataBind", &HybridRiveViewSpec::getDataBind); diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp index c015acab..b6bce968 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp @@ -19,6 +19,8 @@ namespace margelo::nitro::rive { class HybridRiveFileSpec; } namespace margelo::nitro::rive { enum class Alignment; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `FrameRateRange` to properly resolve imports. +namespace margelo::nitro::rive { struct FrameRateRange; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -38,11 +40,12 @@ namespace margelo::nitro::rive { struct UnifiedRiveEvent; } #include "HybridRiveFileSpec.hpp" #include "Alignment.hpp" #include "Fit.hpp" +#include "FrameRateRange.hpp" +#include #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" -#include #include "RiveError.hpp" #include #include @@ -89,6 +92,8 @@ namespace margelo::nitro::rive { virtual void setFit(std::optional fit) = 0; virtual std::optional getLayoutScaleFactor() = 0; virtual void setLayoutScaleFactor(std::optional layoutScaleFactor) = 0; + virtual std::optional> getFrameRate() = 0; + virtual void setFrameRate(const std::optional>& frameRate) = 0; virtual std::optional getSemantics() = 0; virtual void setSemantics(std::optional semantics) = 0; virtual std::optional, DataBindMode, DataBindByName>> getDataBind() = 0; diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp index 048e34e5..570176df 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp @@ -31,6 +31,7 @@ namespace margelo::nitro::rive::views { const react::RawValue* rawValue = rawProps.at("artboardName", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.artboardName; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.artboardName); return CachedProp>::fromRawValue(*runtime, value, sourceProps.artboardName); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.artboardName: ") + exc.what()); @@ -41,6 +42,7 @@ namespace margelo::nitro::rive::views { const react::RawValue* rawValue = rawProps.at("stateMachineName", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.stateMachineName; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.stateMachineName); return CachedProp>::fromRawValue(*runtime, value, sourceProps.stateMachineName); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.stateMachineName: ") + exc.what()); @@ -51,6 +53,7 @@ namespace margelo::nitro::rive::views { const react::RawValue* rawValue = rawProps.at("autoPlay", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.autoPlay; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.autoPlay); return CachedProp>::fromRawValue(*runtime, value, sourceProps.autoPlay); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.autoPlay: ") + exc.what()); @@ -71,6 +74,7 @@ namespace margelo::nitro::rive::views { const react::RawValue* rawValue = rawProps.at("alignment", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.alignment; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.alignment); return CachedProp>::fromRawValue(*runtime, value, sourceProps.alignment); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.alignment: ") + exc.what()); @@ -81,6 +85,7 @@ namespace margelo::nitro::rive::views { const react::RawValue* rawValue = rawProps.at("fit", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.fit; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.fit); return CachedProp>::fromRawValue(*runtime, value, sourceProps.fit); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.fit: ") + exc.what()); @@ -91,16 +96,29 @@ namespace margelo::nitro::rive::views { const react::RawValue* rawValue = rawProps.at("layoutScaleFactor", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.layoutScaleFactor; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.layoutScaleFactor); return CachedProp>::fromRawValue(*runtime, value, sourceProps.layoutScaleFactor); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.layoutScaleFactor: ") + exc.what()); } }()), + frameRate([&]() -> CachedProp>> { + try { + const react::RawValue* rawValue = rawProps.at("frameRate", nullptr, nullptr); + if (rawValue == nullptr) return sourceProps.frameRate; + const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.frameRate); + return CachedProp>>::fromRawValue(*runtime, value, sourceProps.frameRate); + } catch (const std::exception& exc) { + throw std::runtime_error(std::string("RiveView.frameRate: ") + exc.what()); + } + }()), semantics([&]() -> CachedProp> { try { const react::RawValue* rawValue = rawProps.at("semantics", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.semantics; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.semantics); return CachedProp>::fromRawValue(*runtime, value, sourceProps.semantics); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.semantics: ") + exc.what()); @@ -111,6 +129,7 @@ namespace margelo::nitro::rive::views { const react::RawValue* rawValue = rawProps.at("dataBind", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.dataBind; const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp, DataBindMode, DataBindByName>>>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.dataBind); return CachedProp, DataBindMode, DataBindByName>>>::fromRawValue(*runtime, value, sourceProps.dataBind); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.dataBind: ") + exc.what()); @@ -146,6 +165,7 @@ namespace margelo::nitro::rive::views { case hashString("alignment"): return true; case hashString("fit"): return true; case hashString("layoutScaleFactor"): return true; + case hashString("frameRate"): return true; case hashString("semantics"): return true; case hashString("dataBind"): return true; case hashString("onError"): return true; diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp index 59742f91..564ffcc9 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp @@ -22,11 +22,12 @@ #include "HybridRiveFileSpec.hpp" #include "Alignment.hpp" #include "Fit.hpp" +#include "FrameRateRange.hpp" +#include #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" -#include #include "RiveError.hpp" #include #include "HybridRiveViewSpec.hpp" @@ -58,6 +59,7 @@ namespace margelo::nitro::rive::views { CachedProp> alignment; CachedProp> fit; CachedProp> layoutScaleFactor; + CachedProp>> frameRate; CachedProp> semantics; CachedProp, DataBindMode, DataBindByName>>> dataBind; CachedProp> onError; diff --git a/nitrogen/generated/shared/json/RiveViewConfig.json b/nitrogen/generated/shared/json/RiveViewConfig.json index c2d5d94b..1283b0d9 100644 --- a/nitrogen/generated/shared/json/RiveViewConfig.json +++ b/nitrogen/generated/shared/json/RiveViewConfig.json @@ -11,6 +11,7 @@ "alignment": true, "fit": true, "layoutScaleFactor": true, + "frameRate": true, "semantics": true, "dataBind": true, "onError": true, diff --git a/scripts/nitrogen-postprocess.ts b/scripts/nitrogen-postprocess.ts index 280b7fc7..0cec6394 100644 --- a/scripts/nitrogen-postprocess.ts +++ b/scripts/nitrogen-postprocess.ts @@ -6,6 +6,10 @@ const MANAGER_FILE = join( ROOT, 'nitrogen/generated/android/kotlin/com/margelo/nitro/rive/views/HybridRiveViewManager.kt' ); +const COMPONENT_FILE = join( + ROOT, + 'nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp' +); function makeHybridRiveViewManagerOpen() { if (!existsSync(MANAGER_FILE)) { @@ -28,4 +32,42 @@ function makeHybridRiveViewManagerOpen() { console.log('Made HybridRiveViewManager open'); } +// Fabric's prop diff sends `null` (not `undefined`) when a prop is removed, +// but Nitro's JSIConverter> only maps `undefined` to nullopt, +// so clearing an optional prop throws e.g. "RiveView.artboardName: Value is +// null, expected a String". Treat null like undefined for optional props. +// Remove once fixed upstream: https://github.com/mrousavy/nitro/issues/1184 +function acceptNullForOptionalProps() { + if (!existsSync(COMPONENT_FILE)) { + console.warn('HybridRiveViewComponent.cpp not found, skipping'); + return; + } + + const content = readFileSync(COMPONENT_FILE, 'utf-8'); + const pattern = + /^( *)return (CachedProp>)::fromRawValue\(\*runtime, value, (sourceProps\.\w+)\);$/gm; + const updated = content.replace( + pattern, + (match, indent, cachedProp, sourceProp) => + `${indent}if (value.isNull()) return ${cachedProp}::fromRawValue(*runtime, jsi::Value::undefined(), ${sourceProp});\n${match}` + ); + + if (content === updated) { + if (content.includes('value.isNull()')) { + console.log('HybridRiveViewComponent.cpp already accepts null props'); + } else { + console.warn( + 'No optional CachedProp parse sites found in HybridRiveViewComponent.cpp — nitrogen output may have changed shape' + ); + } + return; + } + + writeFileSync(COMPONENT_FILE, updated); + console.log( + 'Patched HybridRiveViewComponent.cpp to accept null for optional props' + ); +} + makeHybridRiveViewManagerOpen(); +acceptNullForOptionalProps(); diff --git a/src/core/RiveView.tsx b/src/core/RiveView.tsx index 4023e4df..5a314350 100644 --- a/src/core/RiveView.tsx +++ b/src/core/RiveView.tsx @@ -35,6 +35,7 @@ const defaultOnError = (error: RiveError) => * @property {boolean} [autoPlay=true] - Whether to automatically start playing the state machine * @property {Alignment} [alignment] - How the Rive graphic should be aligned within its container * @property {Fit} [fit] - How the Rive graphic should fit within its container + * @property {number | FrameRateRange} [frameRate] - Preferred frame rate for the render loop (experimental backends only) * @property {Object} [style] - React Native style object for container customization * @property {(error: RiveError) => void} [onError] - Callback function that is called when an error occurs * diff --git a/src/index.tsx b/src/index.tsx index fa28c7f9..bbcfa4ee 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,7 @@ export { NitroRiveView } from './core/NitroRiveViewComponent'; export { RiveView, type RiveViewProps } from './core/RiveView'; export type { RiveViewMethods }; export type RiveViewRef = HybridView; +export type { FrameRateRange } from './specs/RiveView.nitro'; export type { RiveFile, RiveEnumDefinition, diff --git a/src/specs/RiveView.nitro.ts b/src/specs/RiveView.nitro.ts index b5a262bc..cfd0d0a3 100644 --- a/src/specs/RiveView.nitro.ts +++ b/src/specs/RiveView.nitro.ts @@ -19,6 +19,16 @@ export interface DataBindByName { byName: string; } +/** + * A frame rate range for the render loop, mirroring CAFrameRateRange. + * The system picks a rate between minimum and maximum, ideally preferred. + */ +export interface FrameRateRange { + minimum: number; + maximum: number; + preferred?: number; +} + /** * Props interface for the RiveView component. * Extends HybridViewProps to include Rive-specific properties. @@ -38,6 +48,21 @@ export interface RiveViewProps extends HybridViewProps { fit?: Fit; /** The scale factor to apply to the Rive graphic when using Fit.Layout */ layoutScaleFactor?: number; + /** + * Preferred frame rate for the render loop. A number caps rendering at that + * many frames per second; a FrameRateRange maps to CAFrameRateRange on iOS, + * while Android applies it best-effort as a cap at preferred ?? maximum. + * Useful to reduce the CPU/battery cost of long-running looping animations + * that don't need the display's full refresh rate (e.g. loaders on 120Hz + * screens). Capping limits frame production, not animation time — playback + * still advances by the real elapsed time between frames. + * + * Only supported on the experimental (default) backends; the legacy + * backends ignore it. Undefined = render at the display refresh rate. + * + * @see https://rive.app/docs/runtimes/apple/apple#frame-rate + */ + frameRate?: number | FrameRateRange; /** * Exposes accessibility semantics authored in the Rive editor to the * platform screen reader (VoiceOver). Defaults to Semantics.Off.