From d631629d4d7b5816f18146b785d35333ec056bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 17 Jul 2026 11:04:11 +0200 Subject: [PATCH 1/4] fix: accept null when optional RiveView props are cleared Fabric's prop diff sends null (not undefined) when a prop is removed, but Nitro's JSIConverter> only maps undefined to nullopt, so clearing any optional view prop threw during commit (upstream mrousavy/nitro#1184). Post-process the generated prop parser to treat null like undefined. Mirrors open PR #326; needed here so frameRate can be cleared back to uncapped. The regenerated output lands with the frameRate commit. --- scripts/nitrogen-postprocess.ts | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) 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(); From db4767651dacf46c3cc2e28483d12a60e0dbd770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 17 Jul 2026 11:04:28 +0200 Subject: [PATCH 2/4] feat: frameRate prop to cap the render loop on the experimental backends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional frameRate view prop (issue #332): a number caps rendering at that many frames per second, and a { minimum, maximum, preferred? } range maps to CAFrameRateRange. Capping limits frame production, not animation time — playback still advances by the real elapsed time. iOS forwards it to RiveUIView.frameRate (.fps / .range / .default, https://rive.app/docs/runtimes/apple/apple#frame-rate). Android has no public per-view API on its new runtime (RiveFramePacer is internal and only drives the Compose loop), so our Choreographer loop skips vsyncs until the cap period has elapsed — same math as upstream's pacer, with half a 120Hz vsync of tolerance so caps land on vsync multiples — and honors a range best-effort as a cap at preferred ?? maximum. On API 35+ the cap is also applied as the platform requestedFrameRate hint while actively drawing, mirroring upstream. While paused, the Android loop now only draws when content changed (initial frame, resize, rebinding) instead of re-rendering identical frames every vsync. The legacy backends accept the prop and ignore it. Measured via dumpsys gfxinfo (Android emulator) and simctl recordVideo + mpdecimate (iOS simulator) on a looping animation: uncapped ~57/58 fps, cap=5 -> 5 fps, range 10-20 preferred 15 -> 15 fps on both platforms, paused -> 0 frames on Android. --- .../com/margelo/nitro/rive/HybridRiveView.kt | 4 + .../com/margelo/nitro/rive/HybridRiveView.kt | 11 +++ .../new/java/com/rive/RiveReactNativeView.kt | 61 +++++++++++++ ios/legacy/HybridRiveView.swift | 3 + ios/new/HybridRiveView.swift | 17 ++++ ios/new/RiveReactNativeView.swift | 7 ++ .../generated/android/c++/JFrameRateRange.hpp | 65 +++++++++++++ .../android/c++/JHybridRiveViewSpec.cpp | 16 +++- .../android/c++/JHybridRiveViewSpec.hpp | 2 + .../c++/JVariant_Double_FrameRateRange.cpp | 26 ++++++ .../c++/JVariant_Double_FrameRateRange.hpp | 70 ++++++++++++++ .../c++/views/JHybridRiveViewStateUpdater.cpp | 4 + .../com/margelo/nitro/rive/FrameRateRange.kt | 44 +++++++++ .../margelo/nitro/rive/HybridRiveViewSpec.kt | 6 ++ .../rive/Variant_Double_FrameRateRange.kt | 53 +++++++++++ .../generated/android/rive+autolinking.cmake | 1 + .../generated/ios/RNRive-Swift-Cxx-Bridge.hpp | 47 ++++++++++ .../ios/RNRive-Swift-Cxx-Umbrella.hpp | 3 + .../ios/c++/HybridRiveViewSpecSwift.hpp | 12 ++- .../ios/c++/views/HybridRiveViewComponent.mm | 5 + .../generated/ios/swift/FrameRateRange.swift | 52 +++++++++++ .../ios/swift/HybridRiveViewSpec.swift | 1 + .../ios/swift/HybridRiveViewSpec_cxx.swift | 43 +++++++++ .../swift/Variant_Double_FrameRateRange.swift | 18 ++++ .../generated/shared/c++/FrameRateRange.hpp | 91 +++++++++++++++++++ .../shared/c++/HybridRiveViewSpec.cpp | 2 + .../shared/c++/HybridRiveViewSpec.hpp | 7 +- .../c++/views/HybridRiveViewComponent.cpp | 20 ++++ .../c++/views/HybridRiveViewComponent.hpp | 4 +- .../generated/shared/json/RiveViewConfig.json | 1 + src/core/RiveView.tsx | 1 + src/index.tsx | 1 + src/specs/RiveView.nitro.ts | 25 +++++ 33 files changed, 719 insertions(+), 4 deletions(-) create mode 100644 nitrogen/generated/android/c++/JFrameRateRange.hpp create mode 100644 nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.cpp create mode 100644 nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.hpp create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/rive/FrameRateRange.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Double_FrameRateRange.kt create mode 100644 nitrogen/generated/ios/swift/FrameRateRange.swift create mode 100644 nitrogen/generated/ios/swift/Variant_Double_FrameRateRange.swift create mode 100644 nitrogen/generated/shared/c++/FrameRateRange.hpp 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..f4c10d3c 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,25 @@ 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 +194,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 +215,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 +245,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 +428,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 +436,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 +451,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { fun playIfNeeded() { paused = false + updateFrameRateHint() } fun setNumberInputValue(name: String, value: Double, path: String?) { 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/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. From 4826b00fe56de1833c3eff20a628c0b0140ebd73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 17 Jul 2026 11:04:41 +0200 Subject: [PATCH 3/4] feat(example): Frame Rate Cap page to exercise the frameRate prop Looping rewards animation with Uncapped / 30 / 15 / 5 fps / range buttons, plus play-pause and remount. Lower caps visibly step while wall-clock playback speed is unchanged; used to verify the cap on both platforms. --- example/src/reproducers/FrameRateCap.tsx | 183 +++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 example/src/reproducers/FrameRateCap.tsx 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', + }, +}); From 23f8d70bdfc7cf520f8a2f3037fc4a7d20144a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 17 Jul 2026 12:39:04 +0200 Subject: [PATCH 4/4] style: break the cap-period chain per ktlint chain-method-continuation --- android/src/new/java/com/rive/RiveReactNativeView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index f4c10d3c..dff96577 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -165,7 +165,8 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { return } - val capPeriodNs = frameRate?.takeIf { it > 0 } + 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