diff --git a/example/assets/lists_demo.rev b/example/assets/lists_demo.rev new file mode 100644 index 00000000..c56bc3ca Binary files /dev/null and b/example/assets/lists_demo.rev differ diff --git a/example/assets/lists_demo.riv b/example/assets/lists_demo.riv new file mode 100644 index 00000000..489c6f4c Binary files /dev/null and b/example/assets/lists_demo.riv differ diff --git a/example/src/pages/DataBindingListExample.tsx b/example/src/pages/DataBindingListExample.tsx deleted file mode 100644 index 4dc2b599..00000000 --- a/example/src/pages/DataBindingListExample.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { - View, - Text, - StyleSheet, - ActivityIndicator, - TouchableOpacity, -} from 'react-native'; -import { useMemo, useState, useCallback, useRef } from 'react'; -import { - Fit, - RiveView, - type ViewModelInstance, - type RiveFile, - type RiveViewRef, - useRiveFile, -} from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; - -export default function DataBindingListExample() { - const { riveFile, isLoading, error } = useRiveFile( - require('../../assets/list.riv') - ); - - return ( - - - {isLoading ? ( - - ) : riveFile ? ( - - ) : ( - {error || 'Unexpected error'} - )} - - - ); -} - -function WithViewModelSetup({ file }: { file: RiveFile }) { - const viewModel = useMemo(() => file.viewModelByName('menu VM'), [file]); - const instance = useMemo( - () => viewModel?.createDefaultInstance(), - [viewModel] - ); - - if (!instance || !viewModel) { - return ( - - {!viewModel - ? "No view model 'menu VM' found" - : 'Failed to create view model instance'} - - ); - } - - return ; -} - -function ListExample({ - instance, - file, -}: { - instance: ViewModelInstance; - file: RiveFile; -}) { - const riveRef = useRef(null); - const [isPlaying, setIsPlaying] = useState(true); - const listProperty = useMemo( - () => instance.listProperty('ListItemVM'), - [instance] - ); - const [listLength, setListLength] = useState(listProperty?.length ?? 0); - - const refreshLength = useCallback(() => { - setListLength(listProperty?.length ?? 0); - }, [listProperty]); - - const handleAddItem = useCallback(() => { - if (!listProperty) return; - const buttonVM = file.viewModelByName('button VM'); - if (!buttonVM) { - console.error('button VM view model not found'); - return; - } - const newInstance = buttonVM.createInstance(); - if (!newInstance) { - console.error('Failed to create new button VM instance'); - return; - } - const stringProp = newInstance.stringProperty('string'); - if (stringProp) { - stringProp.value = 'new btn'; - } - listProperty.addInstance(newInstance); - riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, file, refreshLength]); - - const handleRemoveFirst = useCallback(() => { - if (!listProperty || listProperty.length === 0) return; - listProperty.removeInstanceAt(0); - riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, refreshLength]); - - const handleRemoveLast = useCallback(() => { - if (!listProperty || listProperty.length === 0) return; - listProperty.removeInstanceAt(listProperty.length - 1); - riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, refreshLength]); - - const handleSwapFirstTwo = useCallback(() => { - if (!listProperty || listProperty.length < 2) return; - listProperty.swap(0, 1); - riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, refreshLength]); - - const logListItems = useCallback(() => { - if (!listProperty) return; - console.log(`List has ${listProperty.length} items:`); - for (let i = 0; i < listProperty.length; i++) { - const item = listProperty.getInstanceAt(i); - console.log(` [${i}]: ${item?.instanceName ?? 'undefined'}`); - } - }, [listProperty]); - - const handlePlayPause = useCallback(() => { - if (isPlaying) { - riveRef.current?.pause(); - } else { - riveRef.current?.play(); - } - setIsPlaying(!isPlaying); - }, [isPlaying]); - - if (!listProperty) { - return ( - ListItemVM list property not found - ); - } - - return ( - - { - riveRef.current = ref; - }, - }} - style={styles.rive} - autoPlay={true} - dataBind={instance} - fit={Fit.Contain} - file={file} - /> - - List length: {listLength} - - - Add Item - - - Remove First - - - - - Remove Last - - - Swap 0↔1 - - - - - - {isPlaying ? 'Pause' : 'Play'} - - - - Log Items - - - - - ); -} - -DataBindingListExample.metadata = { - name: 'Data Binding Lists', - description: 'Test data binding with list properties (ViewModelListProperty)', -} satisfies Metadata; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - }, - riveContainer: { - flex: 1, - backgroundColor: '#f5f5f5', - }, - rive: { - flex: 1, - width: '100%', - }, - controls: { - padding: 16, - backgroundColor: '#f0f0f0', - }, - infoText: { - fontSize: 16, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 12, - }, - buttonRow: { - flexDirection: 'row', - justifyContent: 'space-around', - marginBottom: 8, - }, - button: { - backgroundColor: '#007AFF', - paddingVertical: 10, - paddingHorizontal: 16, - borderRadius: 8, - minWidth: 120, - }, - playButton: { - backgroundColor: '#34C759', - }, - logButton: { - backgroundColor: '#666', - }, - buttonText: { - color: '#fff', - fontSize: 14, - fontWeight: 'bold', - textAlign: 'center', - }, - errorText: { - color: 'red', - textAlign: 'center', - padding: 20, - }, -}); diff --git a/example/src/pages/MenuListExample.tsx b/example/src/pages/MenuListExample.tsx new file mode 100644 index 00000000..6d5d4436 --- /dev/null +++ b/example/src/pages/MenuListExample.tsx @@ -0,0 +1,314 @@ +import { + View, + Text, + StyleSheet, + ActivityIndicator, + TouchableOpacity, + TextInput, + ScrollView, +} from 'react-native'; +import { useRef, useMemo } from 'react'; +import { + Fit, + RiveView, + type ViewModelInstance, + type RiveFile, + useRiveFile, + useRiveList, + useViewModelInstance, +} from '@rive-app/react-native'; +import { type Metadata } from '../helpers/metadata'; + +export default function MenuListExample() { + const { riveFile, isLoading, error } = useRiveFile( + require('../../assets/lists_demo.riv') + ); + + return ( + + {isLoading ? ( + + ) : riveFile ? ( + + ) : ( + {error || 'Unexpected error'} + )} + + ); +} + +function MenuList({ file }: { file: RiveFile }) { + const instance = useViewModelInstance(file, { required: true }); + + if (!instance) { + return ; + } + + return ; +} + +function MenuListContent({ + file, + instance, +}: { + file: RiveFile; + instance: ViewModelInstance; +}) { + const addLabelRef = useRef(null); + const lastAdded = useRef(null); + const indexToDeleteRef = useRef(null); + const index1Ref = useRef(null); + const index2Ref = useRef(null); + const updateIndexRef = useRef(null); + const updateLabelRef = useRef(null); + + const addLabelValue = useRef('New Item'); + const indexToDeleteValue = useRef('0'); + const index1Value = useRef('0'); + const index2Value = useRef('1'); + const updateIndexValue = useRef('0'); + const updateLabelValue = useRef('Updated Item'); + + const { + length, + addInstance, + removeInstance, + removeInstanceAt, + getInstanceAt, + swap, + error, + } = useRiveList('menu', instance); + + const listItemViewModel = useMemo( + () => file.viewModelByName('listItem'), + [file] + ); + + const addNewMenuItem = (label: string) => { + if (!listItemViewModel) return; + + const newMenuItemVmi = listItemViewModel.createInstance(); + if (!newMenuItemVmi) return; + + const labelProperty = newMenuItemVmi.stringProperty('label'); + const hoverColorProperty = newMenuItemVmi.colorProperty('hoverColor'); + const fontIconProperty = newMenuItemVmi.stringProperty('fontIcon'); + + if (!labelProperty || !hoverColorProperty || !fontIconProperty) return; + + labelProperty.value = label; + hoverColorProperty.value = 0xff323232; + fontIconProperty.value = ''; + + lastAdded.current = newMenuItemVmi; + addInstance(newMenuItemVmi); + }; + + const removeLastAdded = () => { + if (lastAdded.current) { + removeInstance(lastAdded.current); + lastAdded.current = null; + } + }; + + const removeByIndex = (index: number) => { + removeInstanceAt(index); + }; + + const swapIndexes = (index1: number, index2: number) => { + swap(index1, index2); + }; + + const updateLabelAtIndex = (index: number, label: string) => { + const menuItem = getInstanceAt(index); + if (!menuItem) return; + + const menuItemLabel = menuItem.stringProperty('label'); + if (!menuItemLabel) return; + + menuItemLabel.value = label; + }; + + return ( + + + + + Menu Items: {length} + {error && {error.message}} + + + (addLabelValue.current = text)} + /> + addNewMenuItem(addLabelValue.current)} + > + Add Menu Item + + + Delete Last Added + + + + + (indexToDeleteValue.current = text)} + /> + + removeByIndex(parseInt(indexToDeleteValue.current, 10)) + } + > + Remove by Index + + + + + (index1Value.current = text)} + /> + (index2Value.current = text)} + /> + + swapIndexes( + parseInt(index1Value.current, 10), + parseInt(index2Value.current, 10) + ) + } + > + Swap Indexes + + + + + (updateIndexValue.current = text)} + /> + (updateLabelValue.current = text)} + /> + + updateLabelAtIndex( + parseInt(updateIndexValue.current, 10), + updateLabelValue.current + ) + } + > + Update Label + + + + + ); +} + +MenuListExample.metadata = { + name: 'Menu List', + description: 'Data binding lists demo adapted from rive-react codesandbox', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#1a1a2e', + }, + rive: { + height: 350, + width: '100%', + }, + controls: { + flex: 1, + padding: 16, + backgroundColor: '#16213e', + }, + listLength: { + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 16, + color: '#fff', + }, + controlGroup: { + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + gap: 8, + marginBottom: 12, + paddingBottom: 12, + borderBottomWidth: 1, + borderBottomColor: '#333', + }, + input: { + flex: 1, + minWidth: 100, + backgroundColor: '#fff', + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 6, + fontSize: 14, + }, + inputSmall: { + width: 50, + backgroundColor: '#fff', + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 6, + fontSize: 14, + textAlign: 'center', + }, + button: { + backgroundColor: '#0f4c75', + paddingVertical: 10, + paddingHorizontal: 14, + borderRadius: 6, + }, + buttonText: { + color: '#fff', + fontSize: 13, + fontWeight: 'bold', + }, + errorText: { + color: 'red', + textAlign: 'center', + padding: 20, + fontSize: 16, + }, +}); diff --git a/example/src/pages/index.ts b/example/src/pages/index.ts index 83ee0f79..3bc2b0c0 100644 --- a/example/src/pages/index.ts +++ b/example/src/pages/index.ts @@ -9,4 +9,4 @@ export { default as OutOfBandAssetsWithSuspense } from './OutOfBandAssetsWithSus export { default as ManyViewModels } from './ManyViewModels'; export { default as ResponsiveLayouts } from './ResponsiveLayouts'; export { default as SharedValueListenerExample } from './SharedValueListenerExample'; -export { default as DataBindingListExample } from './DataBindingListExample'; +export { default as MenuListExample } from './MenuListExample'; diff --git a/src/hooks/useRiveColor.ts b/src/hooks/useRiveColor.ts index 3e9a9c30..00b8ad37 100644 --- a/src/hooks/useRiveColor.ts +++ b/src/hooks/useRiveColor.ts @@ -3,7 +3,6 @@ import type { ViewModelColorProperty, ViewModelInstance, } from '../specs/ViewModel.nitro'; -import { type UseRivePropertyResult } from '../types'; import { useRiveProperty } from './useRiveProperty'; import { RiveColor } from '../core/RiveColor'; @@ -11,6 +10,17 @@ const COLOR_PROPERTY_OPTIONS = { getProperty: (vmi: ViewModelInstance, p: string) => vmi.colorProperty(p), }; +type ColorInput = RiveColor | string; +type ColorSetValueAction = + | ColorInput + | ((prevValue: RiveColor | undefined) => ColorInput); + +export interface UseRiveColorResult { + value: RiveColor | undefined; + setValue: (value: ColorSetValueAction) => void; + error: Error | null; +} + /** * Hook for interacting with color ViewModel instance properties. * @@ -21,9 +31,7 @@ const COLOR_PROPERTY_OPTIONS = { export function useRiveColor( path: string, viewModelInstance?: ViewModelInstance | null -): UseRivePropertyResult & { - setValue: (value: RiveColor | string) => void; -} { +): UseRiveColorResult { const [rawValue, setRawValue, error] = useRiveProperty< ViewModelColorProperty, number @@ -33,12 +41,20 @@ export function useRiveColor( rawValue !== undefined ? RiveColor.fromInt(rawValue) : undefined; const setValue = useCallback( - (newValue: RiveColor | string) => { - const color = - typeof newValue === 'string' - ? RiveColor.fromHexString(newValue) - : newValue; - setRawValue(color.toInt()); + (valueOrUpdater: ColorSetValueAction) => { + setRawValue((prevRaw: number | undefined) => { + const prevColor = + prevRaw !== undefined ? RiveColor.fromInt(prevRaw) : undefined; + const newColorInput = + typeof valueOrUpdater === 'function' + ? valueOrUpdater(prevColor) + : valueOrUpdater; + const color = + typeof newColorInput === 'string' + ? RiveColor.fromHexString(newColorInput) + : newColorInput; + return color.toInt(); + }); }, [setRawValue] ); diff --git a/src/hooks/useRiveList.ts b/src/hooks/useRiveList.ts new file mode 100644 index 00000000..71d09785 --- /dev/null +++ b/src/hooks/useRiveList.ts @@ -0,0 +1,108 @@ +import { useCallback, useEffect, useState, useMemo } from 'react'; +import type { ViewModelInstance } from '../specs/ViewModel.nitro'; +import type { UseRiveListResult } from '../types'; + +/** + * Hook for interacting with list ViewModel instance properties. + * + * @param path - The path to the list property + * @param viewModelInstance - The ViewModelInstance containing the list property + * @returns An object with list length, manipulation methods, and error state + */ +export function useRiveList( + path: string, + viewModelInstance?: ViewModelInstance | null +): UseRiveListResult { + const [error, setError] = useState(null); + const [revision, setRevision] = useState(0); + + useEffect(() => { + setError(null); + }, [path, viewModelInstance]); + + const property = useMemo(() => { + if (!viewModelInstance) return undefined; + return viewModelInstance.listProperty(path); + }, [viewModelInstance, path]); + + useEffect(() => { + if (viewModelInstance && !property) { + setError( + new Error(`List property "${path}" not found in the ViewModel instance`) + ); + } + }, [viewModelInstance, property, path]); + + useEffect(() => { + if (!property) return; + + const removeListener = property.addListener(() => { + setRevision((r) => r + 1); + }); + + return () => { + removeListener(); + property.removeListeners(); + property.dispose(); + }; + }, [property]); + + const length = useMemo(() => { + // revision is used to trigger re-computation when list changes + revision; + return property?.length ?? 0; + }, [property, revision]); + + const getInstanceAt = useCallback( + (index: number) => { + return property?.getInstanceAt(index); + }, + [property] + ); + + const addInstance = useCallback( + (instance: ViewModelInstance) => { + property?.addInstance(instance); + }, + [property] + ); + + const addInstanceAt = useCallback( + (instance: ViewModelInstance, index: number) => { + return property?.addInstanceAt(instance, index) ?? false; + }, + [property] + ); + + const removeInstance = useCallback( + (instance: ViewModelInstance) => { + property?.removeInstance(instance); + }, + [property] + ); + + const removeInstanceAt = useCallback( + (index: number) => { + property?.removeInstanceAt(index); + }, + [property] + ); + + const swap = useCallback( + (index1: number, index2: number) => { + return property?.swap(index1, index2) ?? false; + }, + [property] + ); + + return { + length, + getInstanceAt, + addInstance, + addInstanceAt, + removeInstance, + removeInstanceAt, + swap, + error, + }; +} diff --git a/src/hooks/useRiveProperty.ts b/src/hooks/useRiveProperty.ts index fed29ab8..96141314 100644 --- a/src/hooks/useRiveProperty.ts +++ b/src/hooks/useRiveProperty.ts @@ -28,7 +28,12 @@ export function useRiveProperty

( /** Optional override callback for property events (mainly used by triggers) */ onPropertyEventOverride?: (...args: any[]) => void; } -): [T | undefined, (value: T) => void, Error | null, P | undefined] { +): [ + T | undefined, + (value: T | ((prevValue: T | undefined) => T)) => void, + Error | null, + P | undefined, +] { const [value, setValue] = useState(undefined); const [error, setError] = useState(null); @@ -79,7 +84,7 @@ export function useRiveProperty

( // Set the value of the property const setPropertyValue = useCallback( - (newValue: T) => { + (valueOrUpdater: T | ((prevValue: T | undefined) => T)) => { if (!property) { setError( new Error( @@ -87,6 +92,12 @@ export function useRiveProperty

( ) ); } else { + const newValue = + typeof valueOrUpdater === 'function' + ? (valueOrUpdater as (prevValue: T | undefined) => T)( + property.value + ) + : valueOrUpdater; property.value = newValue; } }, diff --git a/src/hooks/useViewModelInstance.ts b/src/hooks/useViewModelInstance.ts new file mode 100644 index 00000000..7666dfc2 --- /dev/null +++ b/src/hooks/useViewModelInstance.ts @@ -0,0 +1,195 @@ +import { useMemo, useEffect, useRef } from 'react'; +import type { ViewModel, ViewModelInstance } from '../specs/ViewModel.nitro'; +import type { RiveFile } from '../specs/RiveFile.nitro'; +import type { RiveViewRef } from '../index'; +import { callDispose } from '../core/callDispose'; + +export interface UseViewModelInstanceParams { + /** + * Get a specifically named instance from the ViewModel. + */ + name?: string; + /** + * Create a new (blank) instance from the ViewModel. + */ + useNew?: boolean; + /** + * If true, throws an error when the instance cannot be obtained. + * This is useful with Error Boundaries and ensures TypeScript knows + * the return value is non-null. + */ + required?: boolean; + /** + * Called synchronously when a new instance is created, before the hook returns. + * Use this to set initial values that need to be available immediately. + * Note: This callback is excluded from deps - changing it won't recreate the instance. + */ + onInit?: (instance: ViewModelInstance) => void; +} + +type ViewModelSource = ViewModel | RiveFile | RiveViewRef; + +function isRiveViewRef(source: ViewModelSource | null): source is RiveViewRef { + return ( + source !== null && source !== undefined && 'getViewModelInstance' in source + ); +} + +function isRiveFile(source: ViewModelSource | null): source is RiveFile { + return ( + source !== null && + source !== undefined && + 'defaultArtboardViewModel' in source + ); +} + +function createInstance( + source: ViewModelSource | null, + name: string | undefined, + useNew: boolean +): { instance: ViewModelInstance | null; needsDispose: boolean } { + if (!source) { + return { instance: null, needsDispose: false }; + } + + if (isRiveViewRef(source)) { + const vmi = source.getViewModelInstance(); + return { instance: vmi ?? null, needsDispose: false }; + } + + if (isRiveFile(source)) { + const viewModel = source.defaultArtboardViewModel(); + const vmi = viewModel?.createDefaultInstance(); + return { instance: vmi ?? null, needsDispose: true }; + } + + // ViewModel source + let vmi: ViewModelInstance | undefined; + if (name) { + vmi = source.createInstanceByName(name); + } else if (useNew) { + vmi = source.createInstance(); + } else { + vmi = source.createDefaultInstance(); + } + return { instance: vmi ?? null, needsDispose: true }; +} + +/** + * Hook for getting a ViewModelInstance from a RiveFile, ViewModel, or RiveViewRef. + * + * @param source - The RiveFile, ViewModel, or RiveViewRef to get an instance from + * @param params - Configuration for which instance to retrieve (only used with ViewModel) + * @returns The ViewModelInstance or null if not found + * + * @example + * ```tsx + * // From RiveFile (get default instance) + * const { riveFile } = useRiveFile(require('./animation.riv')); + * const instance = useViewModelInstance(riveFile); + * ``` + * + * @example + * ```tsx + * // From RiveViewRef (get auto-bound instance) + * const { riveViewRef, setHybridRef } = useRive(); + * const instance = useViewModelInstance(riveViewRef); + * ``` + * + * @example + * ```tsx + * // From ViewModel + * const viewModel = file.viewModelByName('main'); + * const instance = useViewModelInstance(viewModel); + * ``` + * + * @example + * ```tsx + * // Create a new blank instance from ViewModel + * const viewModel = file.viewModelByName('TodoItem'); + * const newInstance = useViewModelInstance(viewModel, { useNew: true }); + * ``` + * + * @example + * ```tsx + * // With required: true (throws if null, use with Error Boundary) + * const instance = useViewModelInstance(riveFile, { required: true }); + * // instance is guaranteed to be non-null here + * ``` + * + * @example + * ```tsx + * // With onInit to set initial values synchronously + * const instance = useViewModelInstance(riveFile, { + * onInit: (vmi) => { + * vmi.numberProperty('count').set(initialCount); + * vmi.stringProperty('name').set(userName); + * } + * }); + * // Values are already set here + * ``` + */ +export function useViewModelInstance( + source: ViewModelSource, + params: UseViewModelInstanceParams & { required: true } +): ViewModelInstance; +export function useViewModelInstance( + source: ViewModelSource | null, + params?: UseViewModelInstanceParams +): ViewModelInstance | null; +export function useViewModelInstance( + source: ViewModelSource | null, + params?: UseViewModelInstanceParams +): ViewModelInstance | null { + const name = params?.name; + const useNew = params?.useNew ?? false; + const required = params?.required ?? false; + const onInit = params?.onInit; + + const prevInstanceRef = useRef<{ + instance: ViewModelInstance | null; + needsDispose: boolean; + } | null>(null); + + const result = useMemo(() => { + const created = createInstance(source, name, useNew); + if (created.instance && onInit) { + onInit(created.instance); + } + return created; + // eslint-disable-next-line react-hooks/exhaustive-deps -- onInit excluded intentionally + }, [source, name, useNew]); + + // Dispose previous instance if it changed and needed disposal + if ( + prevInstanceRef.current && + prevInstanceRef.current.instance !== result.instance && + prevInstanceRef.current.needsDispose && + prevInstanceRef.current.instance + ) { + callDispose(prevInstanceRef.current.instance); + } + prevInstanceRef.current = result; + + // Cleanup on unmount + useEffect(() => { + return () => { + if ( + prevInstanceRef.current?.needsDispose && + prevInstanceRef.current.instance + ) { + callDispose(prevInstanceRef.current.instance); + prevInstanceRef.current = null; + } + }; + }, []); + + if (required && result.instance === null) { + throw new Error( + 'useViewModelInstance: Failed to get ViewModelInstance. ' + + 'Ensure the source has a valid ViewModel and instance available.' + ); + } + + return result.instance; +} diff --git a/src/index.tsx b/src/index.tsx index 5896add0..0a5a3a4f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -48,6 +48,9 @@ export { useRiveBoolean } from './hooks/useRiveBoolean'; export { useRiveEnum } from './hooks/useRiveEnum'; export { useRiveColor } from './hooks/useRiveColor'; export { useRiveTrigger } from './hooks/useRiveTrigger'; +export { useRiveList } from './hooks/useRiveList'; +export { useViewModelInstance } from './hooks/useViewModelInstance'; export { useRiveFile } from './hooks/useRiveFile'; export { type RiveFileInput } from './hooks/useRiveFile'; +export { type SetValueAction } from './types'; export { DataBindMode }; diff --git a/src/types.tsx b/src/types.tsx index 2fd165bf..11577fdb 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -1,13 +1,22 @@ +/** + * A value or a function that computes a new value from the previous value. + * Similar to React's SetStateAction pattern. + */ +export type SetValueAction = T | ((prevValue: T | undefined) => T); + export interface UseRivePropertyResult { /** * The current value of the property. */ value: T | undefined; /** - * Set the value of the property. - * @param value - The value to set the property to. + * Set the value of the property. Accepts either a direct value or + * a function that receives the previous value and returns the new value. + * @example + * setValue(10) // Set to 10 + * setValue((prev) => (prev ?? 0) + 5) // Increment by 5 */ - setValue: (value: T) => void; + setValue: (value: SetValueAction) => void; /** * The error if the property is not found. */ @@ -32,3 +41,49 @@ export interface UseRiveTriggerResult { export type UseViewModelInstanceTriggerParameters = { onTrigger?: () => void; }; + +export interface UseRiveListResult { + /** + * The number of instances in the list. + */ + length: number; + /** + * Get the instance at the given index. + */ + getInstanceAt: ( + index: number + ) => import('./specs/ViewModel.nitro').ViewModelInstance | undefined; + /** + * Add an instance to the end of the list. + */ + addInstance: ( + instance: import('./specs/ViewModel.nitro').ViewModelInstance + ) => void; + /** + * Add an instance at the given index. + * @returns true if successful + */ + addInstanceAt: ( + instance: import('./specs/ViewModel.nitro').ViewModelInstance, + index: number + ) => boolean; + /** + * Remove an instance from the list. + */ + removeInstance: ( + instance: import('./specs/ViewModel.nitro').ViewModelInstance + ) => void; + /** + * Remove the instance at the given index. + */ + removeInstanceAt: (index: number) => void; + /** + * Swap the instances at the given indices. + * @returns true if successful + */ + swap: (index1: number, index2: number) => boolean; + /** + * The error if the property is not found. + */ + error: Error | null; +}