using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; using Rive.Utils; namespace Rive { /// /// Represents a runtime instance of a view model with mutable property values. /// A ViewModelInstance contains the same properties as its source view model but maintains its own state that can change during execution. /// public sealed class ViewModelInstance : ViewModelInstanceProperty, IDisposable { private ViewModelInstanceSafeHandle m_safeHandle; private WeakReference m_riveFile; // Strong references to subscribed properties keyed by native pointer. // The VMI owns these so that properties survive even when the user drops their reference. private readonly Dictionary m_subscribedProperties = new Dictionary(); private readonly List> m_parents = new List>(); private readonly List m_children = new List(); // caching nested view model instances by name private readonly Dictionary m_viewModelInstances = new Dictionary(); private const char kPathSeparator = '/'; /// /// Cache for split paths to avoid repeated string operations /// private static readonly ConcurrentDictionary s_pathSegmentsCache = new ConcurrentDictionary(); private bool m_disposed = false; internal bool IsDisposed => m_disposed; private string m_viewModelName = null; internal File RiveFile { get { if (m_riveFile != null && m_riveFile.TryGetTarget(out var file)) { return file; } return null; } } internal ViewModelInstanceSafeHandle NativeSafeHandle => m_safeHandle; internal string ViewModelName { get { if (m_viewModelName == null && !m_disposed) { m_viewModelName = Marshal.PtrToStringAnsi(getViewModelNameFromViewModelInstance(NativeSafeHandle)); } return m_viewModelName; } } private ViewModelInstance(IntPtr instanceValue, File riveFile) { m_safeHandle = new ViewModelInstanceSafeHandle(instanceValue); m_riveFile = new WeakReference(riveFile); } ~ViewModelInstance() { Dispose(false); } private static string[] GetPathSegments(string path) { // For very frequent calls, caching the split results can improve performance // If the user tries to get all the properties of a view model instance, this can be called a lot // We cache the split results to avoid repeated string operations if (!s_pathSegmentsCache.TryGetValue(path, out var segments)) { segments = path.Split(kPathSeparator); s_pathSegmentsCache[path] = segments; } return segments; } private T GetPropertyFromPathSegments(string[] pathSegments, int index) where T : ViewModelInstanceProperty { if (index < pathSegments.Length - 1) { // We need to navigate to a nested view model instance so we can propagate callbacks var nestedInstance = GetInternalViewModelInstance(pathSegments[index]); if (nestedInstance != null) { return nestedInstance.GetPropertyFromPathSegments(pathSegments, index + 1); } else { return null; } } // We're at the final segment, get the property directly return ViewModelInstancePropertyHandlersFactory.GetPrimitiveProperty(this, pathSegments[index]); } private ViewModelInstance GetViewModelInstanceFromPathSegments(string[] pathSegments, int index) { if (index >= pathSegments.Length) { return this; } var viewModelInstance = GetInternalViewModelInstance(pathSegments[index]); if (viewModelInstance != null) { if (index == pathSegments.Length - 1) { return viewModelInstance; } else { return viewModelInstance.GetViewModelInstanceFromPathSegments(pathSegments, index + 1); } } return null; } private bool HasParent(ViewModelInstance parent) { for (int i = 0; i < m_parents.Count; i++) { if (m_parents[i].TryGetTarget(out var existingParent) && existingParent == parent) { return true; } } return false; } private ViewModelInstance GetInternalViewModelInstance(string name) { if (m_viewModelInstances.TryGetValue(name, out var instance)) { return instance; } // Otherwise, create and cache it var ptr = getViewModelInstanceViewModelProperty(NativeSafeHandle, name); if (ptr != IntPtr.Zero) { if (TryGetCachedViewModelInstanceForPointer(ptr, out var cachedInstance)) { // If we have already created this instance for this pointer, use it m_viewModelInstances[name] = cachedInstance; // Let's make sure the parent relationship is set if (!cachedInstance.HasParent(this)) { cachedInstance.AddParent(this); } return cachedInstance; } var newInstance = GetOrCreateFromPointer(ptr, RiveFile, this); m_viewModelInstances[name] = newInstance; return newInstance; } return null; } /// /// Gets a nested view model instance property. /// /// The path to the nested property. If the property is on the current instance, the path is the property name. /// The nested view model instance property. private ViewModelInstance GetNestedViewModelInstance(string path) { // Fast path for simple names (no path separator) if (!path.Contains(kPathSeparator)) { return GetInternalViewModelInstance(path); } string[] pathSegments = GetPathSegments(path); return GetViewModelInstanceFromPathSegments(pathSegments, 0); } /// /// Replaces a nested view model instance property with a new instance. /// /// The name of the property to replace. /// The new view model instance to replace the property with. /// True if the view model property was replaced, false otherwise. E.g. If the view model instance provided is for a different view model, the replacement will fail. private bool InternalReplaceViewModel(string name, ViewModelInstance value) { if (value == null || value.NativeSafeHandle.IsInvalid) { return false; } bool result = replaceViewModelInstanceViewModelProperty(NativeSafeHandle, name, value.NativeSafeHandle); if (result) { // Clean up the old instance if it exists if (m_viewModelInstances.TryGetValue(name, out var oldInstance)) { oldInstance.RemoveParent(this); // Remove from children list if present if (m_children.Contains(oldInstance)) { m_children.Remove(oldInstance); } } m_viewModelInstances[name] = value; value.AddParent(this); } return result; } private void ClearCallbacks() { foreach (var kvp in m_subscribedProperties) { kvp.Value.ClearDelegatesOnly(); PropertyCallbacksHub.Instance.Unregister(kvp.Key); } m_subscribedProperties.Clear(); } internal void AddParent(ViewModelInstance parent) { // Check if parent already exists if (HasParent(parent)) { return; } m_parents.Add(new WeakReference(parent)); // If we have properties or children with callbacks, notify parent if (m_subscribedProperties.Count > 0 || m_children.Count > 0) { parent.AddChildToCallbacks(this); } } internal void RemoveParent(ViewModelInstance parent) { for (int i = m_parents.Count - 1; i >= 0; i--) { if (m_parents[i].TryGetTarget(out var existingParent) && existingParent == parent) { parent.RemoveChildFromCallbacks(this); m_parents.RemoveAt(i); return; } } } internal void AddChildToCallbacks(ViewModelInstance child) { if (!m_children.Contains(child)) { m_children.Add(child); // Propagate up to parents for (int i = 0; i < m_parents.Count; i++) { var parent = m_parents[i]; if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null) { parentInstance.AddChildToCallbacks(this); } } } } internal void RemoveChildFromCallbacks(ViewModelInstance child) { m_children.Remove(child); // If no more children or properties need callbacks, notify parents if (m_children.Count == 0 && m_subscribedProperties.Count == 0) { for (int i = 0; i < m_parents.Count; i++) { var parent = m_parents[i]; if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null) { parentInstance.RemoveChildFromCallbacks(this); } } } } /// /// Called by a property when the user subscribes to it for callbacks. /// This is used to notify parents that they need to subscribe to this property for callbacks as well. /// internal void RegisterPropertyForCallbacks(ViewModelInstancePrimitiveProperty property) { if (property == null) { return; } IntPtr ptr = property.InstancePropertyPtr; if (ptr == IntPtr.Zero) { return; } bool wasFirst = m_subscribedProperties.Count == 0; bool added = !m_subscribedProperties.ContainsKey(ptr); m_subscribedProperties[ptr] = property; PropertyCallbacksHub.Instance.Register(property); if (added && wasFirst) { for (int i = 0; i < m_parents.Count; i++) { var parent = m_parents[i]; if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null) { parentInstance.AddChildToCallbacks(this); } } } } /// /// Called by a property when it transitions from non-zero to zero subscribers. /// internal void UnregisterPropertyForCallbacks(ViewModelInstancePrimitiveProperty property) { if (property == null) { return; } IntPtr ptr = property.InstancePropertyPtr; if (ptr != IntPtr.Zero) { m_subscribedProperties.Remove(ptr); PropertyCallbacksHub.Instance.Unregister(ptr); } // If no more properties with callbacks and no children with callbacks, notify parents if (m_subscribedProperties.Count == 0 && m_children.Count == 0) { for (int i = 0; i < m_parents.Count; i++) { var parent = m_parents[i]; if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null) { parentInstance.RemoveChildFromCallbacks(this); } } } } /// /// Gets a cached nested view model instance for a given pointer. This is used to avoid creating multiple C# instances of the same underlying native instance. /// internal static bool TryGetCachedViewModelInstanceForPointer(IntPtr ptr, out ViewModelInstance instance) { if (ViewModelInstanceProperty.TryGetGloballyCachedVMPropertyForPointer(ptr, out var property)) { instance = property as ViewModelInstance; if (instance != null) { return true; } return false; } instance = null; return false; } /// /// Removes a cached view model instance for a given pointer. /// /// internal static void RemoveGloballyCachedViewModelInstanceForPointer(IntPtr ptr) { ViewModelInstanceProperty.RemoveCachedPropertyForPointer(ptr); } /// /// Adds a cached view model instance for a given pointer. This is used to avoid creating multiple C# instances of the same underlying native instance. /// /// /// internal static void AddCachedViewModelInstanceForPointer(IntPtr ptr, ViewModelInstance instance) { ViewModelInstanceProperty.AddGloballyCachedVMPropertyForPointer(ptr, instance); } /// /// Gets a property of the view model instance. /// /// The type of the property to get. /// The path to the property. If the property is on the current instance, the path is the property name. /// The path can be a nested path, e.g. "nestedInstance/propertyName". /// The property of the view model instance. public T GetProperty(string path) where T : ViewModelInstanceProperty { if (string.IsNullOrEmpty(path)) { DebugLogger.Instance.LogError("Property path cannot be null or empty"); return null; } if (m_disposed) { throw new ObjectDisposedException(nameof(ViewModelInstance), "Cannot get property from a disposed ViewModelInstance."); } // Handle ViewModelInstance type specially since we do a few things differently for non-primitive properties if (typeof(T) == typeof(ViewModelInstance)) { return GetNestedViewModelInstance(path) as T; } // Fast path for simple property names (no nested path separator) if (!path.Contains(kPathSeparator)) { return ViewModelInstancePropertyHandlersFactory.GetPrimitiveProperty(this, path); } string[] pathSegments = GetPathSegments(path); return GetPropertyFromPathSegments(pathSegments, 0); } /// /// Detects property value changes /// Call this after advancing wherever you handle your per-frame logic. /// public void HandleCallbacks() { if (m_disposed) { return; } foreach (var kvp in m_subscribedProperties) { var prop = kvp.Value; if (prop.HasChanged) { prop.RaiseChangedEvent(); } } foreach (var kvp in m_subscribedProperties) { var prop = kvp.Value; if (prop.HasChanged) { prop.ClearChanges(); } } // Propagate to children for (int i = 0; i < m_children.Count; i++) { m_children[i]?.HandleCallbacks(); } } /// /// Replaces a nested view model instance property with a new instance. /// /// The path to the property to replace. /// The new instance to replace the property with. public void SetViewModelInstance(string path, ViewModelInstance newInstance) { if (m_disposed) { throw new ObjectDisposedException(nameof(ViewModelInstance), "Cannot set view model instance on a disposed ViewModelInstance."); } if (string.IsNullOrEmpty(path)) { DebugLogger.Instance.LogError("Property path cannot be null or empty"); return; } bool wasReplaced = false; // Fast path for simple names (no path separator) if (!path.Contains(kPathSeparator)) { wasReplaced = InternalReplaceViewModel(path, newInstance); if (!wasReplaced) { DebugLogger.Instance.LogError($"Failed to replace nested view model instance property at path: {path}. The property may not exist or the new instance may be of a different view model type."); } return; } string[] pathSegments = GetPathSegments(path); ViewModelInstance currentViewModel = this; // Navigate to the parent of the target instance (all segments except the last) for (int i = 0; i < pathSegments.Length - 1; i++) { currentViewModel = currentViewModel.GetInternalViewModelInstance(pathSegments[i]); if (currentViewModel == null) { DebugLogger.Instance.LogError($"View model not found at segment '{pathSegments[i]}' in path: {path}"); return; } } // Now currentViewModel is the parent of our target, so lets replace the final segment wasReplaced = currentViewModel.InternalReplaceViewModel( pathSegments[pathSegments.Length - 1], newInstance); if (!wasReplaced) { DebugLogger.Instance.LogError($"Failed to replace nested view model instance property at path: {path}. The property may not exist or the new instance may be of a different view model type."); } } #region Convenience methods /// /// Gets a number property of the view model instance. /// /// The path to the property. /// The number property, or null if the property doesn't exist or is not a number. public ViewModelInstanceNumberProperty GetNumberProperty(string path) { return GetProperty(path); } /// /// Gets a boolean property of the view model instance. /// /// The path to the property. /// The boolean property, or null if the property doesn't exist or is not a boolean. public ViewModelInstanceBooleanProperty GetBooleanProperty(string path) { return GetProperty(path); } /// /// Gets a string property of the view model instance. /// /// The path to the property. /// The string property, or null if the property doesn't exist or is not a string. public ViewModelInstanceStringProperty GetStringProperty(string path) { return GetProperty(path); } /// /// Gets a color property of the view model instance. /// /// The path to the property. /// The color property, or null if the property doesn't exist or is not a color. public ViewModelInstanceColorProperty GetColorProperty(string path) { return GetProperty(path); } /// /// Gets an enum property of the view model instance. /// /// The path to the property. /// The enum property, or null if the property doesn't exist or is not an enum. public ViewModelInstanceEnumProperty GetEnumProperty(string path) { return GetProperty(path); } /// /// Gets a trigger property of the view model instance. /// /// The path to the property. /// The trigger property, or null if the property doesn't exist or is not a trigger. public ViewModelInstanceTriggerProperty GetTriggerProperty(string path) { return GetProperty(path); } /// /// Gets an image property of the view model instance. /// /// The path to the property. /// The image property, or null if the property doesn't exist or is not an image. public ViewModelInstanceImageProperty GetImageProperty(string path) { return GetProperty(path); } /// /// Gets a list property of the view model instance. /// /// The path to the property. /// The list property, or null if the property doesn't exist or is not a list. public ViewModelInstanceListProperty GetListProperty(string path) { return GetProperty(path); } /// /// Gets an artboard property of the view model instance. /// /// The path to the property. /// The artboard property, or null if the property doesn't exist or is not an artboard. public ViewModelInstanceArtboardProperty GetArtboardProperty(string path) { return GetProperty(path); } /// /// Gets a nested view model instance property. /// /// The path to the property. /// The nested view model instance, or null if the property doesn't exist or is not a view model. public ViewModelInstance GetViewModelInstanceProperty(string path) { return GetProperty(path); } #endregion private void Dispose(bool disposing) { if (m_disposed) { return; } if (disposing) { // ClearCallbacks() is intentionally only called on the explicit Dispose() path. // On the finalizer path, the managed objects in m_subscribedProperties may already be finalized, // and taking the lock inside PropertyCallbacksHub.Unregister() from a finalizer thread risks deadlocks. // The hub uses weak references, so the hub won't keep the properties alive. It will also clean up any dead properties during the next CaptureChanges() call. ClearCallbacks(); foreach (var kvp in m_viewModelInstances) { var childViewModelInstance = kvp.Value; childViewModelInstance.RemoveParent(this); } m_viewModelInstances.Clear(); for (int i = m_parents.Count - 1; i >= 0; i--) { if (m_parents[i].TryGetTarget(out var parentInstance) && parentInstance != null) { RemoveParent(parentInstance); } } m_children.Clear(); } if (m_safeHandle != null && !m_safeHandle.IsInvalid) { // Get the IntPtr for cache removal before disposing IntPtr nativePtr = m_safeHandle.DangerousGetHandle(); m_safeHandle.Dispose(); RemoveGloballyCachedViewModelInstanceForPointer(nativePtr); } m_disposed = true; if (disposing) { GC.SuppressFinalize(this); } } public void Dispose() { Dispose(true); } /// /// Helper method to get or create a ViewModelInstance from a native pointer. /// This method checks if the instance already exists in the cache. If it does, it returns the existing instance so that a single C# instance is always used for the same native instance no matter which method returns it. /// If it doesn't exist, it creates a new ViewModelInstance and adds it to the cache. /// /// The native pointer to the ViewModelInstance. /// The Rive file associated with the ViewModelInstance. This is used to resolve the file context for the instance. /// The parent ViewModelInstance, if any. The parent is used to propagate callbacks to this instance. A vm instance can have multiple parents. /// The ViewModelInstance associated with the native pointer. internal static ViewModelInstance GetOrCreateFromPointer(IntPtr instancePtr, File riveFile, ViewModelInstance parent = null) { if (TryGetCachedViewModelInstanceForPointer(instancePtr, out ViewModelInstance existingInstance)) { // Unity already owns this - balance the extra ref from underlying native methods. // If we don't do this, the native instance might stay in memory longer than intended. ViewModelInstanceSafeHandle.unrefViewModelInstance(instancePtr); if (parent != null) { existingInstance.AddParent(parent); } return existingInstance; } var newInstance = new ViewModelInstance(instancePtr, riveFile); if (parent != null) { newInstance.AddParent(parent); } AddCachedViewModelInstanceForPointer(instancePtr, newInstance); return newInstance; } #region Native Calls [DllImport(NativeLibrary.name)] private static extern nuint getViewModelInstancePropertyCount(ViewModelInstanceSafeHandle instanceValue); [DllImport(NativeLibrary.name)] private static extern IntPtr getViewModelInstancePropertyAtPath(ViewModelInstanceSafeHandle instanceValue, string path); [DllImport(NativeLibrary.name)] private static extern IntPtr getViewModelInstanceViewModelProperty(ViewModelInstanceSafeHandle instanceValue, string path); [DllImport(NativeLibrary.name)] private static extern IntPtr getViewModelNameFromViewModelInstance(ViewModelInstanceSafeHandle instanceValue); /// /// Replaces a nested view model instance property with a new instance. /// /// The instance that contains the property to replace. /// The path to the property to replace. /// The new instance to replace the property with. /// True if the view model property was replaced, false otherwise. E.g. If the view model instance provided is for a different view model, the /// replacement will fail. [DllImport(NativeLibrary.name)] private static extern bool replaceViewModelInstanceViewModelProperty( ViewModelInstanceSafeHandle baseInstanceValue, string path, ViewModelInstanceSafeHandle newInstance); #endregion } /// /// SafeHandle implementation for ViewModelInstance native resources /// internal sealed class ViewModelInstanceSafeHandle : SafeHandleZeroOrMinusOneIsInvalid { // The P/Invoke marshaller throws ArgumentNullException if a SafeHandle argument is null. // We use this reusable invalid handle to represent IntPtr.Zero for optional parameters. internal static readonly ViewModelInstanceSafeHandle Null = new ViewModelInstanceSafeHandle(); public ViewModelInstanceSafeHandle() : base(true) { } public ViewModelInstanceSafeHandle(IntPtr handle) : base(true) { SetHandle(handle); } protected override bool ReleaseHandle() { if (!IsInvalid) { unrefViewModelInstance(handle); return true; } return false; } [DllImport(NativeLibrary.name)] internal static extern void unrefViewModelInstance(IntPtr instancePtr); } }