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 f1118170..7292252c 100644 --- a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -94,6 +94,11 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { // experimental backends. override var frameRate: Variant_Double_FrameRateRange? = null + // Accepted for API parity; offscreen handling and draw skipping are only + // implemented on the new runtimes. + override var offscreenBehavior: OffscreenBehavior? = null + override var renderEnabled: Variant_Boolean_String? = 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 d4f7ab0b..04f1add4 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -6,6 +6,7 @@ import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.core.Promise import com.rive.BindData +import com.rive.RenderMode import com.rive.RiveReactNativeView import com.rive.ViewConfiguration import app.rive.Fit as RiveFit @@ -111,6 +112,24 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { ) } + override var offscreenBehavior: OffscreenBehavior? = null + set(value) { + field = value + view.offscreenBehavior = value ?: OffscreenBehavior.NONE + } + + override var renderEnabled: Variant_Boolean_String? = null + set(value) { + field = value + view.renderMode = when { + value == null -> RenderMode.Enabled + value.asSecondOrNull() == "pause" -> RenderMode.Paused + value.asFirstOrNull() == false -> RenderMode.SkipDraws + // true, or an unrecognized string (the public API narrows to 'pause') + else -> RenderMode.Enabled + } + } + // 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 dcfdf423..c16b25aa 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -1,6 +1,7 @@ package com.rive import android.annotation.SuppressLint +import android.graphics.Rect import android.graphics.SurfaceTexture import android.os.Build import android.util.Log @@ -21,6 +22,7 @@ import app.rive.core.RiveSurface import app.rive.core.StateMachineHandle import app.rive.core.SurfaceTextureSurface import com.facebook.react.uimanager.ThemedReactContext +import com.margelo.nitro.rive.OffscreenBehavior import com.margelo.nitro.rive.RiveErrorLogger import com.margelo.nitro.rive.RiveLog import kotlinx.coroutines.CompletableDeferred @@ -34,6 +36,10 @@ import kotlinx.coroutines.withContext import kotlin.time.Duration import kotlin.time.Duration.Companion.nanoseconds +// The renderEnabled prop (boolean | 'pause') resolved into what the render +// loop should do: draw normally, keep advancing but skip draws, or stop both. +enum class RenderMode { Enabled, SkipDraws, Paused } + sealed class BindData { data object None : BindData() data object Auto : BindData() @@ -71,6 +77,24 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { updateFrameRateHint() } + // What to do while the view is outside the visible viewport (scrolled out, + // hidden, or windowless): keep running, skip draws only, or pause fully. + // Never automatic — data-binding consumers may rely on the state machine + // advancing while offscreen. Overlays in the same window (e.g. RN Modal) + // don't count as covering the view. + var offscreenBehavior: OffscreenBehavior = OffscreenBehavior.NONE + + // Manual counterpart to offscreenBehavior for occlusion the view can't + // detect (RN Modal, bottom sheets): SkipDraws keeps the state machine + // advancing, Paused stops it too (composes with the pause()/play() state + // rather than overwriting it). + var renderMode: RenderMode = RenderMode.Enabled + + private val visibleRectBuffer = Rect() + + private fun isInVisibleViewport(): Boolean = + isShown && getGlobalVisibleRect(visibleRectBuffer) + var onError: ((String) -> Unit)? = null // Fired when the state machine settles (reaches rest, e.g. a non-looping @@ -172,7 +196,11 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { override fun doFrame(frameTimeNanos: Long) { if (!renderLoopRunning || disposed) return - if (paused && !needsRedraw) { + val offscreen = offscreenBehavior != OffscreenBehavior.NONE && !isInVisibleViewport() + val pausedOffscreen = offscreen && offscreenBehavior == OffscreenBehavior.PAUSE + val renderPaused = renderMode == RenderMode.Paused + + if ((paused || pausedOffscreen || renderPaused) && !needsRedraw) { // Keep the timebase fresh so resuming advances by one frame, not by // the whole pause span. lastFrameTimeNs = frameTimeNanos @@ -204,17 +232,26 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { val sm = stateMachineHandle val rs = riveSurface + // Offscreen views keep advancing the state machine (events and + // data-binding listeners stay live) and only skip the draw — the draw + // is the dominant part of the offscreen cost. needsRedraw still forces + // a draw so pending content (initial frame, resize, rebinding) isn't + // lost while invisible. + val skipDraw = renderMode != RenderMode.Enabled || offscreen + if (worker != null && art != null && sm != null && rs != null) { try { - if (!paused && !settled) { + if (!paused && !settled && !pausedOffscreen && !renderPaused) { worker.advanceStateMachine(sm, deltaTime) } - worker.draw(art, sm, rs, activeFit) - needsRedraw = false - frameCount++ - val isFirstFrame = frameCount == 1L - if (isFirstFrame) { - viewReadyDeferred.complete(true) + if (!skipDraw || needsRedraw) { + worker.draw(art, sm, rs, activeFit) + needsRedraw = false + frameCount++ + val isFirstFrame = frameCount == 1L + if (isFirstFrame) { + viewReadyDeferred.complete(true) + } } } catch (e: Exception) { Log.e(TAG, "Render loop error", e) diff --git a/example/src/reproducers/OffscreenBehavior.tsx b/example/src/reproducers/OffscreenBehavior.tsx new file mode 100644 index 00000000..4ad1250c --- /dev/null +++ b/example/src/reproducers/OffscreenBehavior.tsx @@ -0,0 +1,290 @@ +import { useRef, useState } from 'react'; +import { + View, + Text, + StyleSheet, + Pressable, + ScrollView, + Modal, +} from 'react-native'; +import { + RiveView, + useRiveFile, + Fit, + type OffscreenBehavior, + type RiveViewRef, +} from '@rive-app/react-native'; +import { type Metadata } from '../shared/metadata'; + +/** + * Manual verifier for offscreenBehavior and renderEnabled (issue #332 + * follow-up). + * + * Puts a looping animation into one of several visibility states so process + * CPU can be sampled externally (e.g. per-thread /proc//task stats on + * Android): + * + * - onscreen: playing, fully visible at the top of the ScrollView + * - offscreen: playing, scrolled fully out of the viewport (still mounted) + * - modal: playing, covered by a full-screen RN Modal + * - paused: pause() via ref, still visible + * - unmounted: RiveView removed from the tree + * + * With offscreenBehavior 'skip-draws' or 'pause' the offscreen scenario + * should drop close to the paused cost; scrolling back must resume the + * animation. The modal scenario is invisible to automatic detection — the + * renderEnabled←modal toggle wires renderEnabled to it (false or 'pause' + * while covered), which is the pattern that prop exists for. + */ + +type Scenario = 'onscreen' | 'offscreen' | 'modal' | 'paused' | 'unmounted'; + +const SCENARIOS: { key: Scenario; label: string }[] = [ + { key: 'onscreen', label: 'Onscreen' }, + { key: 'offscreen', label: 'Offscreen (scrolled)' }, + { key: 'modal', label: 'Covered (modal)' }, + { key: 'paused', label: 'Paused' }, + { key: 'unmounted', label: 'Unmounted' }, +]; + +const BEHAVIORS: OffscreenBehavior[] = ['none', 'skip-draws', 'pause']; + +// What renderEnabled is set to while the modal covers the view: not wired at +// all, draw skipping only, or a declarative full pause. +const RENDER_WIRINGS = ['off', 'skip-draws', 'pause'] as const; +type RenderWiring = (typeof RENDER_WIRINGS)[number]; + +export default function OffscreenBehaviorPage() { + const [scenario, setScenario] = useState('onscreen'); + const [behavior, setBehavior] = useState('none'); + const [renderWiring, setRenderWiring] = useState('off'); + const viewRef = useRef(null); + const scrollRef = useRef(null); + const { riveFile } = useRiveFile(require('../../assets/rive/rewards.riv')); + + const mounted = scenario !== 'unmounted'; + + const applyScenario = (next: Scenario) => { + const prev = scenario; + setScenario(next); + if (prev === 'paused' && next !== 'paused') { + viewRef.current?.play(); + } + if (next === 'paused') { + viewRef.current?.pause(); + } + requestAnimationFrame(() => { + scrollRef.current?.scrollTo({ + y: next === 'offscreen' ? 1200 : 0, + animated: false, + }); + }); + }; + + return ( + + + {scenario.toUpperCase()} · {behavior} + {renderWiring !== 'off' + ? ` +renderEnabled←modal (${renderWiring})` + : ''} + + + {SCENARIOS.map(({ key, label }) => ( + applyScenario(key)} + > + + {label} + + + ))} + + + {BEHAVIORS.map((value) => ( + setBehavior(value)} + > + + {value} + + + ))} + + setRenderWiring( + (v) => + RENDER_WIRINGS[ + (RENDER_WIRINGS.indexOf(v) + 1) % RENDER_WIRINGS.length + ]! + ) + } + > + + renderEnabled←modal: {renderWiring} + + + + + + {mounted && riveFile ? ( + (viewRef.current = ref) }} + style={styles.rive} + /> + ) : ( + + {mounted ? 'Loading…' : 'Unmounted'} + + )} + + + + Scrolled content — the Rive view is above the viewport + + + + applyScenario('onscreen')} + > + + Covering modal + + The Rive view is mounted and playing underneath this modal. + + applyScenario('onscreen')} + > + Close + + + + + ); +} + +OffscreenBehaviorPage.metadata = { + name: 'Offscreen behavior', + description: + 'CPU cost of a playing looping animation when offscreen/covered, and the offscreenBehavior/renderEnabled props that reduce it (issue #332)', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + status: { + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + paddingVertical: 8, + backgroundColor: '#222', + color: '#0f0', + }, + buttonRow: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 6, + paddingHorizontal: 8, + paddingVertical: 4, + }, + button: { + paddingHorizontal: 10, + paddingVertical: 8, + backgroundColor: '#eee', + borderRadius: 8, + }, + buttonActive: { + backgroundColor: '#007AFF', + }, + buttonText: { + color: '#333', + fontWeight: '600', + fontSize: 12, + }, + buttonTextActive: { + color: '#fff', + }, + scroll: { + flex: 1, + }, + riveContainer: { + height: 300, + alignItems: 'center', + justifyContent: 'center', + }, + rive: { + width: 300, + height: 300, + }, + loading: { + fontSize: 16, + color: '#666', + }, + spacer: { + height: 1600, + alignItems: 'center', + paddingTop: 400, + }, + spacerText: { + color: '#999', + paddingHorizontal: 24, + textAlign: 'center', + }, + modalContent: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 12, + backgroundColor: '#fff', + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + }, + modalText: { + color: '#666', + paddingHorizontal: 32, + textAlign: 'center', + }, +}); diff --git a/ios/legacy/HybridRiveView.swift b/ios/legacy/HybridRiveView.swift index 87551008..f88c51e1 100644 --- a/ios/legacy/HybridRiveView.swift +++ b/ios/legacy/HybridRiveView.swift @@ -109,6 +109,10 @@ class HybridRiveView: HybridRiveViewSpec { // Accepted for API parity; frame-rate control is only implemented on the // experimental backends. var frameRate: Variant_Double_FrameRateRange? + // Accepted for API parity; offscreen handling and draw skipping are only + // implemented on the new runtimes. + var offscreenBehavior: OffscreenBehavior? + var renderEnabled: Variant_Bool_String? // 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 c87091d5..e410c883 100644 --- a/ios/new/HybridRiveView.swift +++ b/ios/new/HybridRiveView.swift @@ -99,6 +99,11 @@ class HybridRiveView: HybridRiveViewSpec { var fit: Fit? var layoutScaleFactor: Double? var frameRate: Variant_Double_FrameRateRange? + var offscreenBehavior: OffscreenBehavior? + // 'pause' is honored; the boolean (draw-only skipping) is accepted for API + // parity — the upstream iOS runtime couples advancing and drawing, so + // false behaves like true. + var renderEnabled: Variant_Bool_String? var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } var onStop: () -> Void = {} @@ -216,6 +221,8 @@ class HybridRiveView: HybridRiveViewSpec { fit: toRiveFit(fit, alignment: alignment, layoutScaleFactor: layoutScaleFactor), semantics: toRiveSemantics(semantics), frameRate: toRiveFrameRate(frameRate), + offscreenBehavior: offscreenBehavior ?? .none, + renderPaused: toRenderPaused(renderEnabled), bindData: try dataBind.toBindData() ) @@ -281,6 +288,13 @@ class HybridRiveView: HybridRiveViewSpec { } } + private func toRenderPaused(_ renderEnabled: Variant_Bool_String?) -> Bool { + if case .some(.second("pause")) = renderEnabled { + return true + } + return false + } + 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 bd2c3baf..30b77473 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -17,6 +17,8 @@ struct ViewConfiguration { let fit: RiveRuntime.Fit let semantics: RiveRuntime.Semantics let frameRate: RiveRuntime.FrameRate + let offscreenBehavior: OffscreenBehavior + let renderPaused: Bool let bindData: BindData } @@ -39,6 +41,79 @@ class RiveReactNativeView: UIView { } var autoPlay: Bool = true + // The upstream runtime couples advancing and drawing behind a single + // isPaused flag, so only .pause is implementable here — .skipDraws behaves + // like .none (iOS already throttles most offscreen rendering on its own). + // Never automatic — data-binding consumers may rely on the state machine + // advancing while offscreen. Scrolling produces no callback on this view, + // so visibility is polled on a low-frequency timer that only runs while + // opted in. + private var offscreenBehavior: OffscreenBehavior = .none { + didSet { + guard offscreenBehavior != oldValue else { return } + updateVisibilityPolling() + } + } + private var isOffscreen = false { + didSet { + guard isOffscreen != oldValue else { return } + applyPauseState() + } + } + + // renderEnabled='pause': a declarative full stop for occlusion the view + // can't detect. Kept separate from isPaused so it composes with the + // pause()/play() ref methods instead of overwriting their state. + private var renderPaused = false { + didSet { + guard renderPaused != oldValue else { return } + applyPauseState() + } + } + private var visibilityTimer: Timer? + + private func updateVisibilityPolling() { + visibilityTimer?.invalidate() + visibilityTimer = nil + guard offscreenBehavior == .pause else { + isOffscreen = false + return + } + guard window != nil else { + isOffscreen = true + return + } + isOffscreen = !isInVisibleViewport() + let timer = Timer(timeInterval: 0.25, repeats: true) { [weak self] _ in + MainActor.assumeIsolated { + guard let self else { return } + self.isOffscreen = !self.isInVisibleViewport() + } + } + // .common so polling continues while the user is dragging a scroll view — + // that's exactly when visibility changes. + RunLoop.main.add(timer, forMode: .common) + visibilityTimer = timer + } + + private func isInVisibleViewport() -> Bool { + guard let window, !isHidden, alpha > 0, bounds.width > 0, bounds.height > 0 else { + return false + } + let frameInWindow = convert(bounds, to: window) + return frameInWindow.intersects(window.bounds) + } + + private func applyPauseState() { + riveUIView?.isPaused = + isPaused || renderPaused || (offscreenBehavior == .pause && isOffscreen) + } + + override func didMoveToWindow() { + super.didMoveToWindow() + updateVisibilityPolling() + } + /// Configure failures are reported here (wired to the onError prop). var onLoadError: ((String) -> Void)? @@ -68,6 +143,8 @@ class RiveReactNativeView: UIView { semantics = config.semantics frameRate = config.frameRate + offscreenBehavior = config.offscreenBehavior + renderPaused = config.renderPaused if reload { cleanup() @@ -168,12 +245,12 @@ class RiveReactNativeView: UIView { func play() { isPaused = false - riveUIView?.isPaused = false + applyPauseState() } func pause() { isPaused = true - riveUIView?.isPaused = true + applyPauseState() } func reset() { @@ -184,7 +261,7 @@ class RiveReactNativeView: UIView { func playIfNeeded() { if isPaused { isPaused = false - riveUIView?.isPaused = false + applyPauseState() } } @@ -252,9 +329,9 @@ class RiveReactNativeView: UIView { // reconfigure, which previously caused orphaned draw calls ("state machine // not found") from the old MTKView after removeFromSuperview. existing.rive = rive - existing.isPaused = isPaused existing.semantics = semantics existing.frameRate = frameRate + applyPauseState() } else { let uiView = RiveUIView(rive: rive, isPaused: isPaused) uiView.semantics = semantics @@ -268,6 +345,7 @@ class RiveReactNativeView: UIView { uiView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) self.riveUIView = uiView + applyPauseState() } } @@ -290,6 +368,8 @@ class RiveReactNativeView: UIView { /// awaitViewReady() callers so their promises (which retain this view) /// don't hang forever. func detach() { + visibilityTimer?.invalidate() + visibilityTimer = nil resumeViewReadyContinuations(false) cleanup() } @@ -302,13 +382,16 @@ class RiveReactNativeView: UIView { let settled = settledTask let stopNotify = stopNotifyTask let uiView = riveUIView + let timer = visibilityTimer if Thread.isMainThread { + timer?.invalidate() task?.cancel() settled?.cancel() stopNotify?.cancel() uiView?.removeFromSuperview() } else { DispatchQueue.main.async { + timer?.invalidate() task?.cancel() settled?.cancel() stopNotify?.cancel() diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp index ee2ed1ce..18a093b7 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp @@ -15,6 +15,8 @@ namespace margelo::nitro::rive { enum class Alignment; } namespace margelo::nitro::rive { enum class Fit; } // Forward declaration of `FrameRateRange` to properly resolve imports. namespace margelo::nitro::rive { struct FrameRateRange; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -45,6 +47,9 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include #include "JVariant_Double_FrameRateRange.hpp" #include "JFrameRateRange.hpp" +#include "OffscreenBehavior.hpp" +#include "JOffscreenBehavior.hpp" +#include "JVariant_Boolean_String.hpp" #include "Semantics.hpp" #include "JSemantics.hpp" #include "HybridViewModelInstanceSpec.hpp" @@ -175,6 +180,24 @@ namespace margelo::nitro::rive { static const auto method = _javaPart->javaClassStatic()->getMethod /* frameRate */)>("setFrameRate"); method(_javaPart, frameRate.has_value() ? JVariant_Double_FrameRateRange::fromCpp(frameRate.value()) : nullptr); } + std::optional JHybridRiveViewSpec::getOffscreenBehavior() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("getOffscreenBehavior"); + auto __result = method(_javaPart); + return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt; + } + void JHybridRiveViewSpec::setOffscreenBehavior(std::optional offscreenBehavior) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* offscreenBehavior */)>("setOffscreenBehavior"); + method(_javaPart, offscreenBehavior.has_value() ? JOffscreenBehavior::fromCpp(offscreenBehavior.value()) : nullptr); + } + std::optional> JHybridRiveViewSpec::getRenderEnabled() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("getRenderEnabled"); + auto __result = method(_javaPart); + return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt; + } + void JHybridRiveViewSpec::setRenderEnabled(const std::optional>& renderEnabled) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* renderEnabled */)>("setRenderEnabled"); + method(_javaPart, renderEnabled.has_value() ? JVariant_Boolean_String::fromCpp(renderEnabled.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 91ea8d5c..d7c0b9e5 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp @@ -66,6 +66,10 @@ namespace margelo::nitro::rive { void setLayoutScaleFactor(std::optional layoutScaleFactor) override; std::optional> getFrameRate() override; void setFrameRate(const std::optional>& frameRate) override; + std::optional getOffscreenBehavior() override; + void setOffscreenBehavior(std::optional offscreenBehavior) override; + std::optional> getRenderEnabled() override; + void setRenderEnabled(const std::optional>& renderEnabled) override; std::optional getSemantics() override; void setSemantics(std::optional semantics) override; std::optional, DataBindMode, DataBindByName>> getDataBind() override; diff --git a/nitrogen/generated/android/c++/JOffscreenBehavior.hpp b/nitrogen/generated/android/c++/JOffscreenBehavior.hpp new file mode 100644 index 00000000..f54b6a8d --- /dev/null +++ b/nitrogen/generated/android/c++/JOffscreenBehavior.hpp @@ -0,0 +1,61 @@ +/// +/// JOffscreenBehavior.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 "OffscreenBehavior.hpp" + +namespace margelo::nitro::rive { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "OffscreenBehavior" and the the Kotlin enum "OffscreenBehavior". + */ + struct JOffscreenBehavior final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/OffscreenBehavior;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum OffscreenBehavior. + */ + [[maybe_unused]] + [[nodiscard]] + OffscreenBehavior toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(OffscreenBehavior value) { + static const auto clazz = javaClassStatic(); + switch (value) { + case OffscreenBehavior::NONE: + static const auto fieldNONE = clazz->getStaticField("NONE"); + return clazz->getStaticFieldValue(fieldNONE); + case OffscreenBehavior::SKIP_DRAWS: + static const auto fieldSKIP_DRAWS = clazz->getStaticField("SKIP_DRAWS"); + return clazz->getStaticFieldValue(fieldSKIP_DRAWS); + case OffscreenBehavior::PAUSE: + static const auto fieldPAUSE = clazz->getStaticField("PAUSE"); + return clazz->getStaticFieldValue(fieldPAUSE); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/JVariant_Boolean_String.cpp b/nitrogen/generated/android/c++/JVariant_Boolean_String.cpp new file mode 100644 index 00000000..dce76765 --- /dev/null +++ b/nitrogen/generated/android/c++/JVariant_Boolean_String.cpp @@ -0,0 +1,26 @@ +/// +/// JVariant_Boolean_String.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "JVariant_Boolean_String.hpp" + +namespace margelo::nitro::rive { + /** + * Converts JVariant_Boolean_String to std::variant + */ + std::variant JVariant_Boolean_String::toCpp() const { + if (isInstanceOf(JVariant_Boolean_String_impl::First::javaClassStatic())) { + // It's a `bool` + auto jniValue = static_cast(this)->getValue(); + return static_cast(jniValue); + } else if (isInstanceOf(JVariant_Boolean_String_impl::Second::javaClassStatic())) { + // It's a `std::string` + auto jniValue = static_cast(this)->getValue(); + return jniValue->toStdString(); + } + throw std::invalid_argument("Variant is unknown Kotlin instance!"); + } +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/JVariant_Boolean_String.hpp b/nitrogen/generated/android/c++/JVariant_Boolean_String.hpp new file mode 100644 index 00000000..0f62aebc --- /dev/null +++ b/nitrogen/generated/android/c++/JVariant_Boolean_String.hpp @@ -0,0 +1,68 @@ +/// +/// JVariant_Boolean_String.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 +#include + +namespace margelo::nitro::rive { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ std::variant and the Java class "Variant_Boolean_String". + */ + class JVariant_Boolean_String: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Boolean_String;"; + + static jni::local_ref create_0(jboolean 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(jni::make_jstring(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_Boolean_String_impl { + class First final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Boolean_String$First;"; + + [[nodiscard]] jboolean 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_Boolean_String$Second;"; + + [[nodiscard]] jni::local_ref getValue() const { + static const auto field = javaClassStatic()->getField("value"); + return getFieldValue(field); + } + }; + } // namespace JVariant_Boolean_String_impl +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp index ecfe8860..25a775f7 100644 --- a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp +++ b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp @@ -69,6 +69,14 @@ void JHybridRiveViewStateUpdater::updateViewProps(jni::alias_ref /* hybridView->setFrameRate(props->frameRate.value); props->frameRate.isDirty = false; } + if (props->offscreenBehavior.isDirty) { + hybridView->setOffscreenBehavior(props->offscreenBehavior.value); + props->offscreenBehavior.isDirty = false; + } + if (props->renderEnabled.isDirty) { + hybridView->setRenderEnabled(props->renderEnabled.value); + props->renderEnabled.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/HybridRiveViewSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt index a3e782de..bbd7e66d 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt @@ -75,6 +75,18 @@ abstract class HybridRiveViewSpec: HybridView() { @set:Keep abstract var frameRate: Variant_Double_FrameRateRange? + @get:DoNotStrip + @get:Keep + @set:DoNotStrip + @set:Keep + abstract var offscreenBehavior: OffscreenBehavior? + + @get:DoNotStrip + @get:Keep + @set:DoNotStrip + @set:Keep + abstract var renderEnabled: Variant_Boolean_String? + @get:DoNotStrip @get:Keep @set:DoNotStrip diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/OffscreenBehavior.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/OffscreenBehavior.kt new file mode 100644 index 00000000..0bf12487 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/OffscreenBehavior.kt @@ -0,0 +1,24 @@ +/// +/// OffscreenBehavior.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 enum/union "OffscreenBehavior". + */ +@DoNotStrip +@Keep +enum class OffscreenBehavior(@DoNotStrip @Keep val value: Int) { + NONE(0), + SKIP_DRAWS(1), + PAUSE(2); + + companion object +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Boolean_String.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Boolean_String.kt new file mode 100644 index 00000000..f2517add --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Boolean_String.kt @@ -0,0 +1,53 @@ +/// +/// Variant_Boolean_String.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 "Boolean | String". + */ +@Suppress("ClassName") +@DoNotStrip +sealed class Variant_Boolean_String { + @DoNotStrip + data class First(@DoNotStrip val value: Boolean): Variant_Boolean_String() + @DoNotStrip + data class Second(@DoNotStrip val value: String): Variant_Boolean_String() + + val isFirst: Boolean + get() = this is First + val isSecond: Boolean + get() = this is Second + + fun asFirstOrNull(): Boolean? { + val value = (this as? First)?.value ?: return null + return value + } + fun asSecondOrNull(): String? { + val value = (this as? Second)?.value ?: return null + return value + } + + inline fun match(first: (Boolean) -> R, second: (String) -> R): R { + return when (this) { + is First -> first(value) + is Second -> second(value) + } + } + + companion object { + @JvmStatic + @DoNotStrip + fun create(value: Boolean): Variant_Boolean_String = First(value) + @JvmStatic + @DoNotStrip + fun create(value: String): Variant_Boolean_String = Second(value) + } +} diff --git a/nitrogen/generated/android/rive+autolinking.cmake b/nitrogen/generated/android/rive+autolinking.cmake index 31833465..d7383b7c 100644 --- a/nitrogen/generated/android/rive+autolinking.cmake +++ b/nitrogen/generated/android/rive+autolinking.cmake @@ -68,6 +68,7 @@ target_sources( ../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_Boolean_String.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 f44c2c59..1320332b 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp @@ -66,6 +66,8 @@ namespace margelo::nitro::rive { class HybridViewModelSpec; } namespace margelo::nitro::rive { class HybridViewModelStringPropertySpec; } // Forward declaration of `HybridViewModelTriggerPropertySpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridViewModelTriggerPropertySpec; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `ReferencedAssetsType` to properly resolve imports. namespace margelo::nitro::rive { struct ReferencedAssetsType; } // Forward declaration of `ResolvedReferencedAsset` to properly resolve imports. @@ -165,6 +167,7 @@ namespace RNRive { class HybridViewModelTriggerPropertySpec_cxx; } #include "HybridViewModelSpec.hpp" #include "HybridViewModelStringPropertySpec.hpp" #include "HybridViewModelTriggerPropertySpec.hpp" +#include "OffscreenBehavior.hpp" #include "ReferencedAssetsType.hpp" #include "ResolvedReferencedAsset.hpp" #include "RiveAssetType.hpp" @@ -1005,6 +1008,65 @@ namespace margelo::nitro::rive::bridge::swift { return optional.value(); } + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_OffscreenBehavior_ = std::optional; + inline std::optional create_std__optional_OffscreenBehavior_(const OffscreenBehavior& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_OffscreenBehavior_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline OffscreenBehavior get_std__optional_OffscreenBehavior_(const std::optional& optional) noexcept { + 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_bool__std__string_ final { + std::variant variant; + std__variant_bool__std__string_(std::variant variant): variant(variant) { } + operator std::variant() const noexcept { + return variant; + } + inline size_t index() const noexcept { + return variant.index(); + } + inline bool get_0() const noexcept { + return std::get<0>(variant); + } + inline std::string get_1() const noexcept { + return std::get<1>(variant); + } + }; + inline std__variant_bool__std__string_ create_std__variant_bool__std__string_(bool value) noexcept { + return std__variant_bool__std__string_(value); + } + inline std__variant_bool__std__string_ create_std__variant_bool__std__string_(const std::string& value) noexcept { + return std__variant_bool__std__string_(value); + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__variant_bool__std__string__ = std::optional>; + inline std::optional> create_std__optional_std__variant_bool__std__string__(const std::variant& value) noexcept { + return std::optional>(value); + } + inline bool has_value_std__optional_std__variant_bool__std__string__(const std::optional>& optional) noexcept { + return optional.has_value(); + } + inline std::variant get_std__optional_std__variant_bool__std__string__(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 08c77231..54282881 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp @@ -68,6 +68,8 @@ namespace margelo::nitro::rive { class HybridViewModelSpec; } namespace margelo::nitro::rive { class HybridViewModelStringPropertySpec; } // Forward declaration of `HybridViewModelTriggerPropertySpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridViewModelTriggerPropertySpec; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `ReferencedAssetsType` to properly resolve imports. namespace margelo::nitro::rive { struct ReferencedAssetsType; } // Forward declaration of `ResolvedReferencedAsset` to properly resolve imports. @@ -122,6 +124,7 @@ namespace margelo::nitro::rive { enum class ViewModelPropertyType; } #include "HybridViewModelSpec.hpp" #include "HybridViewModelStringPropertySpec.hpp" #include "HybridViewModelTriggerPropertySpec.hpp" +#include "OffscreenBehavior.hpp" #include "ReferencedAssetsType.hpp" #include "ResolvedReferencedAsset.hpp" #include "RiveAssetType.hpp" diff --git a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp index 167b1ee5..9c195f1c 100644 --- a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp @@ -20,6 +20,8 @@ namespace margelo::nitro::rive { enum class Alignment; } namespace margelo::nitro::rive { enum class Fit; } // Forward declaration of `FrameRateRange` to properly resolve imports. namespace margelo::nitro::rive { struct FrameRateRange; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -45,6 +47,7 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "Fit.hpp" #include "FrameRateRange.hpp" #include +#include "OffscreenBehavior.hpp" #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" @@ -159,6 +162,20 @@ namespace margelo::nitro::rive { inline void setFrameRate(const std::optional>& frameRate) noexcept override { _swiftPart.setFrameRate(frameRate); } + inline std::optional getOffscreenBehavior() noexcept override { + auto __result = _swiftPart.getOffscreenBehavior(); + return __result; + } + inline void setOffscreenBehavior(std::optional offscreenBehavior) noexcept override { + _swiftPart.setOffscreenBehavior(offscreenBehavior); + } + inline std::optional> getRenderEnabled() noexcept override { + auto __result = _swiftPart.getRenderEnabled(); + return __result; + } + inline void setRenderEnabled(const std::optional>& renderEnabled) noexcept override { + _swiftPart.setRenderEnabled(renderEnabled); + } 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 03fdc83e..d4bf6b91 100644 --- a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm +++ b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm @@ -112,6 +112,16 @@ - (void) updateProps:(const std::shared_ptr&)props swiftPart.setFrameRate(newViewProps.frameRate.value); newViewProps.frameRate.isDirty = false; } + // offscreenBehavior: optional + if (newViewProps.offscreenBehavior.isDirty) { + swiftPart.setOffscreenBehavior(newViewProps.offscreenBehavior.value); + newViewProps.offscreenBehavior.isDirty = false; + } + // renderEnabled: optional + if (newViewProps.renderEnabled.isDirty) { + swiftPart.setRenderEnabled(newViewProps.renderEnabled.value); + newViewProps.renderEnabled.isDirty = false; + } // semantics: optional if (newViewProps.semantics.isDirty) { swiftPart.setSemantics(newViewProps.semantics.value); diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift index cdb1a81f..b99de630 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift @@ -18,6 +18,8 @@ public protocol HybridRiveViewSpec_protocol: HybridObject, HybridView { var fit: Fit? { get set } var layoutScaleFactor: Double? { get set } var frameRate: Variant_Double_FrameRateRange? { get set } + var offscreenBehavior: OffscreenBehavior? { get set } + var renderEnabled: Variant_Bool_String? { 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 bba3dd02..1b11efb5 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift @@ -312,6 +312,66 @@ open class HybridRiveViewSpec_cxx { } } + public final var offscreenBehavior: bridge.std__optional_OffscreenBehavior_ { + @inline(__always) + get { + return { () -> bridge.std__optional_OffscreenBehavior_ in + if let __unwrappedValue = self.__implementation.offscreenBehavior { + return bridge.create_std__optional_OffscreenBehavior_(__unwrappedValue) + } else { + return .init() + } + }() + } + @inline(__always) + set { + self.__implementation.offscreenBehavior = newValue.value + } + } + + public final var renderEnabled: bridge.std__optional_std__variant_bool__std__string__ { + @inline(__always) + get { + return { () -> bridge.std__optional_std__variant_bool__std__string__ in + if let __unwrappedValue = self.__implementation.renderEnabled { + return bridge.create_std__optional_std__variant_bool__std__string__({ () -> bridge.std__variant_bool__std__string_ in + switch __unwrappedValue { + case .first(let __value): + return bridge.create_std__variant_bool__std__string_(__value) + case .second(let __value): + return bridge.create_std__variant_bool__std__string_(std.string(__value)) + } + }().variant) + } else { + return .init() + } + }() + } + @inline(__always) + set { + self.__implementation.renderEnabled = { () -> Variant_Bool_String? in + if bridge.has_value_std__optional_std__variant_bool__std__string__(newValue) { + let __unwrapped = bridge.get_std__optional_std__variant_bool__std__string__(newValue) + return { () -> Variant_Bool_String in + let __variant = bridge.std__variant_bool__std__string_(__unwrapped) + switch __variant.index() { + case 0: + let __actual = __variant.get_0() + return .first(__actual) + case 1: + let __actual = __variant.get_1() + return .second(String(__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/OffscreenBehavior.swift b/nitrogen/generated/ios/swift/OffscreenBehavior.swift new file mode 100644 index 00000000..fa88d159 --- /dev/null +++ b/nitrogen/generated/ios/swift/OffscreenBehavior.swift @@ -0,0 +1,44 @@ +/// +/// OffscreenBehavior.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `OffscreenBehavior`, backed by a C++ enum. + */ +public typealias OffscreenBehavior = margelo.nitro.rive.OffscreenBehavior + +public extension OffscreenBehavior { + /** + * Get a OffscreenBehavior for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "none": + self = .none + case "skip-draws": + self = .skipDraws + case "pause": + self = .pause + default: + return nil + } + } + + /** + * Get the String value this OffscreenBehavior represents. + */ + var stringValue: String { + switch self { + case .none: + return "none" + case .skipDraws: + return "skip-draws" + case .pause: + return "pause" + } + } +} diff --git a/nitrogen/generated/ios/swift/Variant_Bool_String.swift b/nitrogen/generated/ios/swift/Variant_Bool_String.swift new file mode 100644 index 00000000..0d912bf7 --- /dev/null +++ b/nitrogen/generated/ios/swift/Variant_Bool_String.swift @@ -0,0 +1,18 @@ +/// +/// Variant_Bool_String.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: `boolean | string` + */ +@frozen +public enum Variant_Bool_String { + case first(Bool) + case second(String) +} diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp index 25276fca..628bd984 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp @@ -30,6 +30,10 @@ namespace margelo::nitro::rive { prototype.registerHybridSetter("layoutScaleFactor", &HybridRiveViewSpec::setLayoutScaleFactor); prototype.registerHybridGetter("frameRate", &HybridRiveViewSpec::getFrameRate); prototype.registerHybridSetter("frameRate", &HybridRiveViewSpec::setFrameRate); + prototype.registerHybridGetter("offscreenBehavior", &HybridRiveViewSpec::getOffscreenBehavior); + prototype.registerHybridSetter("offscreenBehavior", &HybridRiveViewSpec::setOffscreenBehavior); + prototype.registerHybridGetter("renderEnabled", &HybridRiveViewSpec::getRenderEnabled); + prototype.registerHybridSetter("renderEnabled", &HybridRiveViewSpec::setRenderEnabled); 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 7fb6ab7b..ef001c9c 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp @@ -21,6 +21,8 @@ namespace margelo::nitro::rive { enum class Alignment; } namespace margelo::nitro::rive { enum class Fit; } // Forward declaration of `FrameRateRange` to properly resolve imports. namespace margelo::nitro::rive { struct FrameRateRange; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -42,6 +44,7 @@ namespace margelo::nitro::rive { struct UnifiedRiveEvent; } #include "Fit.hpp" #include "FrameRateRange.hpp" #include +#include "OffscreenBehavior.hpp" #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" @@ -94,6 +97,10 @@ namespace margelo::nitro::rive { virtual void setLayoutScaleFactor(std::optional layoutScaleFactor) = 0; virtual std::optional> getFrameRate() = 0; virtual void setFrameRate(const std::optional>& frameRate) = 0; + virtual std::optional getOffscreenBehavior() = 0; + virtual void setOffscreenBehavior(std::optional offscreenBehavior) = 0; + virtual std::optional> getRenderEnabled() = 0; + virtual void setRenderEnabled(const std::optional>& renderEnabled) = 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++/OffscreenBehavior.hpp b/nitrogen/generated/shared/c++/OffscreenBehavior.hpp new file mode 100644 index 00000000..a3f266ad --- /dev/null +++ b/nitrogen/generated/shared/c++/OffscreenBehavior.hpp @@ -0,0 +1,80 @@ +/// +/// OffscreenBehavior.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 + +namespace margelo::nitro::rive { + + /** + * An enum which can be represented as a JavaScript union (OffscreenBehavior). + */ + enum class OffscreenBehavior { + NONE SWIFT_NAME(none) = 0, + SKIP_DRAWS SWIFT_NAME(skipDraws) = 1, + PAUSE SWIFT_NAME(pause) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::rive + +namespace margelo::nitro { + + // C++ OffscreenBehavior <> JS OffscreenBehavior (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::rive::OffscreenBehavior fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("none"): return margelo::nitro::rive::OffscreenBehavior::NONE; + case hashString("skip-draws"): return margelo::nitro::rive::OffscreenBehavior::SKIP_DRAWS; + case hashString("pause"): return margelo::nitro::rive::OffscreenBehavior::PAUSE; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum OffscreenBehavior - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::rive::OffscreenBehavior arg) { + switch (arg) { + case margelo::nitro::rive::OffscreenBehavior::NONE: return JSIConverter::toJSI(runtime, "none"); + case margelo::nitro::rive::OffscreenBehavior::SKIP_DRAWS: return JSIConverter::toJSI(runtime, "skip-draws"); + case margelo::nitro::rive::OffscreenBehavior::PAUSE: return JSIConverter::toJSI(runtime, "pause"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert OffscreenBehavior to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("none"): + case hashString("skip-draws"): + case hashString("pause"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp index 82757d46..f70c0e1d 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp @@ -113,6 +113,28 @@ namespace margelo::nitro::rive::views { throw std::runtime_error(std::string("RiveView.frameRate: ") + exc.what()); } }()), + offscreenBehavior([&]() -> CachedProp> { + try { + const react::RawValue* rawValue = rawProps.at("offscreenBehavior", nullptr, nullptr); + if (rawValue == nullptr) return sourceProps.offscreenBehavior; + const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.offscreenBehavior); + return CachedProp>::fromRawValue(*runtime, value, sourceProps.offscreenBehavior); + } catch (const std::exception& exc) { + throw std::runtime_error(std::string("RiveView.offscreenBehavior: ") + exc.what()); + } + }()), + renderEnabled([&]() -> CachedProp>> { + try { + const react::RawValue* rawValue = rawProps.at("renderEnabled", nullptr, nullptr); + if (rawValue == nullptr) return sourceProps.renderEnabled; + const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.renderEnabled); + return CachedProp>>::fromRawValue(*runtime, value, sourceProps.renderEnabled); + } catch (const std::exception& exc) { + throw std::runtime_error(std::string("RiveView.renderEnabled: ") + exc.what()); + } + }()), semantics([&]() -> CachedProp> { try { const react::RawValue* rawValue = rawProps.at("semantics", nullptr, nullptr); @@ -176,6 +198,8 @@ namespace margelo::nitro::rive::views { case hashString("fit"): return true; case hashString("layoutScaleFactor"): return true; case hashString("frameRate"): return true; + case hashString("offscreenBehavior"): return true; + case hashString("renderEnabled"): 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 27d708de..ced31065 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp @@ -24,6 +24,7 @@ #include "Fit.hpp" #include "FrameRateRange.hpp" #include +#include "OffscreenBehavior.hpp" #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" @@ -60,6 +61,8 @@ namespace margelo::nitro::rive::views { CachedProp> fit; CachedProp> layoutScaleFactor; CachedProp>> frameRate; + CachedProp> offscreenBehavior; + CachedProp>> renderEnabled; 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 bed6f54b..dd8778ec 100644 --- a/nitrogen/generated/shared/json/RiveViewConfig.json +++ b/nitrogen/generated/shared/json/RiveViewConfig.json @@ -12,6 +12,8 @@ "fit": true, "layoutScaleFactor": true, "frameRate": true, + "offscreenBehavior": true, + "renderEnabled": true, "semantics": true, "dataBind": true, "onError": true, diff --git a/src/core/RiveView.tsx b/src/core/RiveView.tsx index e4ceeb26..08acb8c3 100644 --- a/src/core/RiveView.tsx +++ b/src/core/RiveView.tsx @@ -5,7 +5,10 @@ import { callDispose } from './callDispose'; import type { RiveViewRef } from '../index'; export interface RiveViewProps - extends Omit, 'onError' | 'onStop'> { + extends Omit< + ComponentProps, + 'onError' | 'onStop' | 'renderEnabled' + > { onError?: (error: RiveError) => void; /** * Called when the animation/state machine stops playing, e.g. when a @@ -14,6 +17,15 @@ export interface RiveViewProps * animations where you want to navigate away once playback finishes. */ onStop?: () => void; + /** + * Manual control over rendering, for occlusion the view cannot detect + * itself (e.g. covered by a Modal or a bottom sheet): false skips draws + * while the state machine keeps advancing, 'pause' also stops advancing — + * like an imperative pause() that composes with the ref methods. Defaults + * to true. New runtimes only; on iOS false behaves like true ('pause' is + * fully supported). + */ + renderEnabled?: boolean | 'pause'; } const defaultOnError = (error: RiveError) => @@ -45,6 +57,8 @@ const defaultOnStop = () => {}; * @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 (new runtimes only) + * @property {OffscreenBehavior} [offscreenBehavior='none'] - What to do while the view is outside the visible viewport: keep running, keep advancing but skip draws, or pause (new runtimes only) + * @property {boolean | 'pause'} [renderEnabled=true] - false skips drawing while the state machine keeps advancing; 'pause' also stops advancing, for views covered by UI the view can't detect (new runtimes 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 * @property {() => void} [onStop] - Callback function that is called when the animation/state machine stops playing (e.g. reaches the end of a non-looping animation) diff --git a/src/index.tsx b/src/index.tsx index bbcfa4ee..e7921651 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,7 +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 { FrameRateRange, OffscreenBehavior } from './specs/RiveView.nitro'; export type { RiveFile, RiveEnumDefinition, diff --git a/src/specs/RiveView.nitro.ts b/src/specs/RiveView.nitro.ts index 25d7b3dc..97bda2aa 100644 --- a/src/specs/RiveView.nitro.ts +++ b/src/specs/RiveView.nitro.ts @@ -29,6 +29,12 @@ export interface FrameRateRange { preferred?: number; } +/** + * What to do while the view is outside the visible viewport (scrolled out of + * view, hidden, or detached from a window). See RiveViewProps.offscreenBehavior. + */ +export type OffscreenBehavior = 'none' | 'skip-draws' | 'pause'; + /** * Props interface for the RiveView component. * Extends HybridViewProps to include Rive-specific properties. @@ -63,6 +69,52 @@ export interface RiveViewProps extends HybridViewProps { * @see https://rive.app/docs/runtimes/apple/apple#frame-rate */ frameRate?: number | FrameRateRange; + /** + * What to do while the view is outside the visible viewport (scrolled out + * of view, hidden, or detached from a window). Views covered by an overlay + * in the same window (e.g. a React Native Modal) still count as visible — + * use renderEnabled for occlusion the view cannot detect. + * + * - 'none' (default): keep advancing and drawing regardless of visibility. + * - 'skip-draws': keep advancing the state machine but skip drawing frames + * while offscreen. Events, data binding and playback time stay live, so + * this is safe even when other UI is driven from the state machine. + * - 'pause': stop advancing and drawing while offscreen; playback resumes + * from where it left off when the view becomes visible again. The state + * machine does not advance while offscreen, so don't use it when + * data-binding consumers rely on it advancing regardless of visibility. + * + * Only supported on the new (default) runtimes; the legacy backends ignore + * it. On iOS the upstream runtime couples advancing and drawing, so + * 'skip-draws' behaves like 'none' there ('pause' is fully supported, and + * iOS already throttles most offscreen rendering on its own). + */ + offscreenBehavior?: OffscreenBehavior; + /** + * Manual control over rendering, for occlusion the view cannot detect + * itself — e.g. covered by a Modal or a bottom sheet. Use + * offscreenBehavior instead for visibility the view can detect (scrolling, + * hiding). Defaults to true. + * + * - true (default): render normally. + * - false: stop drawing frames while the state machine keeps advancing + * (events, data binding, and playback time stay live). The view repaints + * on the next frame after re-enabling. + * - 'pause': also stop advancing the state machine, like an imperative + * pause(); playback resumes where it left off when the prop changes + * back. Unlike pause()/play() this doesn't touch the playback state the + * ref methods control — the two combine. + * + * Only supported on the new (default) runtimes; the legacy backends ignore + * it. On iOS the upstream runtime couples advancing and drawing, so false + * behaves like true there ('pause' is fully supported, and fullscreen iOS + * modals already hide the covered hierarchy). + * + * Typed as `boolean | string` because Nitro cannot mix string literals + * into a variant; the public RiveView component narrows it to + * `boolean | 'pause'`. Strings other than 'pause' render normally. + */ + renderEnabled?: boolean | string; /** * Exposes accessibility semantics authored in the Rive editor to the * platform screen reader (VoiceOver). Defaults to Semantics.Off.