From 93972670b09525395c98ed541556e45a971f716d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 6 May 2026 09:12:49 +0200 Subject: [PATCH 1/2] Add setViewModelInstance to the experimental ViewModelInstance API --- .../src/bindings/bindings_command_queue.cpp | 23 +++++++++++++++++++ .../main/kotlin/app/rive/ViewModelInstance.kt | 17 ++++++++++++++ .../main/kotlin/app/rive/core/CommandQueue.kt | 21 +++++++++++++++++ .../app/rive/core/CommandQueueBridge.kt | 14 +++++++++++ 4 files changed, 75 insertions(+) diff --git a/kotlin/src/main/cpp/src/bindings/bindings_command_queue.cpp b/kotlin/src/main/cpp/src/bindings/bindings_command_queue.cpp index 0f04a031..61f11f11 100644 --- a/kotlin/src/main/cpp/src/bindings/bindings_command_queue.cpp +++ b/kotlin/src/main/cpp/src/bindings/bindings_command_queue.cpp @@ -1698,6 +1698,29 @@ extern "C" artboardHandle); } + JNIEXPORT void JNICALL + Java_app_rive_core_CommandQueueJNIBridge_cppSetViewModelInstanceProperty( + JNIEnv* env, + jobject, + jlong ref, + jlong jViewModelInstanceHandle, + jstring jPropertyPath, + jlong jValueHandle) + { + auto commandQueue = reinterpret_cast(ref); + auto viewModelInstanceHandle = + handleFromLong( + jViewModelInstanceHandle); + auto propertyPath = JStringToString(env, jPropertyPath); + auto valueHandle = + handleFromLong(jValueHandle); + + commandQueue->setViewModelInstanceNestedViewModel( + viewModelInstanceHandle, + propertyPath, + valueHandle); + } + JNIEXPORT void JNICALL Java_app_rive_core_CommandQueueJNIBridge_cppGetListSize( JNIEnv* env, diff --git a/kotlin/src/main/kotlin/app/rive/ViewModelInstance.kt b/kotlin/src/main/kotlin/app/rive/ViewModelInstance.kt index 4e645166..a94961e5 100644 --- a/kotlin/src/main/kotlin/app/rive/ViewModelInstance.kt +++ b/kotlin/src/main/kotlin/app/rive/ViewModelInstance.kt @@ -336,6 +336,23 @@ class ViewModelInstance internal constructor( setProperty(propertyPath, artboard.artboardHandle, riveWorker::setArtboardProperty) } + /** + * Replaces a nested view model instance property with another instance. + * + * After replacement, both the original and the replacement refer to the same underlying + * instance — changing one changes the other. + * + * ℹ️ Changes to bound Rive elements will not be reflected until the next state machine advance. + * + * @param propertyPath The path to the view model property from this view model instance. Slash + * delimited to refer to nested properties. + * @param instance The view model instance to assign to the property. + */ + fun setViewModelInstance(propertyPath: String, instance: ViewModelInstance) { + RiveLog.d(VM_INSTANCE_TAG) { "Assigning $instance to $propertyPath (${fileHandle})" } + setProperty(propertyPath, instance.instanceHandle, riveWorker::setViewModelInstanceProperty) + } + suspend fun getListSize(propertyPath: String): Int = riveWorker.getListSize(instanceHandle, propertyPath) diff --git a/kotlin/src/main/kotlin/app/rive/core/CommandQueue.kt b/kotlin/src/main/kotlin/app/rive/core/CommandQueue.kt index 2548a386..1889fda8 100644 --- a/kotlin/src/main/kotlin/app/rive/core/CommandQueue.kt +++ b/kotlin/src/main/kotlin/app/rive/core/CommandQueue.kt @@ -1540,6 +1540,27 @@ class CommandQueue( artboardHandle.handle ) + /** + * Assign a view model instance to a nested view model property on the view model instance. + * + * @param viewModelInstanceHandle The handle of the view model instance that the property + * belongs to. + * @param propertyPath The path to the property that should be assigned to. Slash delimited. + * @param valueHandle The handle of the view model instance to assign. + * @throws IllegalStateException If the CommandQueue has been released. + */ + @Throws(IllegalStateException::class) + fun setViewModelInstanceProperty( + viewModelInstanceHandle: ViewModelInstanceHandle, + propertyPath: String, + valueHandle: ViewModelInstanceHandle + ) = bridge.cppSetViewModelInstanceProperty( + cppPointer.pointer, + viewModelInstanceHandle.handle, + propertyPath, + valueHandle.handle + ) + /** * Gets the size of a list property on the view model instance. * diff --git a/kotlin/src/main/kotlin/app/rive/core/CommandQueueBridge.kt b/kotlin/src/main/kotlin/app/rive/core/CommandQueueBridge.kt index 7b033039..fb6593cd 100644 --- a/kotlin/src/main/kotlin/app/rive/core/CommandQueueBridge.kt +++ b/kotlin/src/main/kotlin/app/rive/core/CommandQueueBridge.kt @@ -272,6 +272,13 @@ interface CommandQueueBridge { artboardHandle: Long ) + fun cppSetViewModelInstanceProperty( + pointer: Long, + viewModelInstanceHandle: Long, + propertyPath: String, + valueHandle: Long + ) + fun cppGetListSize( pointer: Long, requestID: Long, @@ -715,6 +722,13 @@ internal class CommandQueueJNIBridge : CommandQueueBridge { artboardHandle: Long ) + external override fun cppSetViewModelInstanceProperty( + pointer: Long, + viewModelInstanceHandle: Long, + propertyPath: String, + valueHandle: Long + ) + external override fun cppGetListSize( pointer: Long, requestID: Long, From 3103aebdc9b98e3e5e8626da0a0e74b3b383248e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 7 May 2026 09:13:24 +0200 Subject: [PATCH 2/2] Add unit tests for setViewModelInstanceProperty --- .../kotlin/app/rive/CommandQueueUnitTest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/kotlin/src/test/kotlin/app/rive/CommandQueueUnitTest.kt b/kotlin/src/test/kotlin/app/rive/CommandQueueUnitTest.kt index ff92be6b..9560158b 100644 --- a/kotlin/src/test/kotlin/app/rive/CommandQueueUnitTest.kt +++ b/kotlin/src/test/kotlin/app/rive/CommandQueueUnitTest.kt @@ -7,6 +7,7 @@ import app.rive.core.DefaultViewModelInfo import app.rive.core.FileHandle import app.rive.core.Listeners import app.rive.core.RenderContext +import app.rive.core.ViewModelInstanceHandle import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.comparables.shouldBeLessThan @@ -29,6 +30,7 @@ const val COMMAND_QUEUE_ADDR = 1L const val RENDER_CONTEXT_ADDR = 2L const val HANDLE_NUM = 123L const val ARTBOARD_HANDLE_NUM = 456L +const val VALUE_HANDLE_NUM = 789L val FILE_BYTES = byteArrayOf(0, 1, 2) @OptIn(ExperimentalCoroutinesApi::class, ExperimentalStdlibApi::class) @@ -228,6 +230,46 @@ class CommandQueueUnitTest : FunSpec({ } } + test("Set view model instance property invokes native") { + val commandQueue = CommandQueue(renderContextMock, commandQueueBridgeMock) + val instanceHandle = ViewModelInstanceHandle(HANDLE_NUM) + val valueHandle = ViewModelInstanceHandle(VALUE_HANDLE_NUM) + val propertyPath = "nested/path" + + every { + commandQueueBridgeMock.cppSetViewModelInstanceProperty( + COMMAND_QUEUE_ADDR, + HANDLE_NUM, + propertyPath, + VALUE_HANDLE_NUM + ) + } just runs + + commandQueue.setViewModelInstanceProperty(instanceHandle, propertyPath, valueHandle) + + verify(exactly = 1) { + commandQueueBridgeMock.cppSetViewModelInstanceProperty( + COMMAND_QUEUE_ADDR, + HANDLE_NUM, + propertyPath, + VALUE_HANDLE_NUM + ) + } + } + + test("Set view model instance property throws when released") { + val commandQueue = CommandQueue(renderContextMock, commandQueueBridgeMock) + commandQueue.release("") + + shouldThrow { + commandQueue.setViewModelInstanceProperty( + ViewModelInstanceHandle(HANDLE_NUM), + "path", + ViewModelInstanceHandle(VALUE_HANDLE_NUM) + ) + } + } + test("Delete file invokes native") { val commandQueue = CommandQueue(renderContextMock, commandQueueBridgeMock) val requestID = slot()