832 lines
31 KiB
C#
832 lines
31 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using Microsoft.Win32.SafeHandles;
|
|
using Rive.Utils;
|
|
|
|
namespace Rive
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class ViewModelInstance : ViewModelInstanceProperty, IDisposable
|
|
{
|
|
private ViewModelInstanceSafeHandle m_safeHandle;
|
|
|
|
private WeakReference<File> 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<IntPtr, ViewModelInstancePrimitiveProperty> m_subscribedProperties = new Dictionary<IntPtr, ViewModelInstancePrimitiveProperty>();
|
|
|
|
private readonly List<WeakReference<ViewModelInstance>> m_parents = new List<WeakReference<ViewModelInstance>>(); private readonly List<ViewModelInstance> m_children = new List<ViewModelInstance>();
|
|
|
|
// caching nested view model instances by name
|
|
private readonly Dictionary<string, ViewModelInstance> m_viewModelInstances = new Dictionary<string, ViewModelInstance>();
|
|
|
|
private const char kPathSeparator = '/';
|
|
|
|
/// <summary>
|
|
/// Cache for split paths to avoid repeated string operations
|
|
/// </summary>
|
|
private static readonly ConcurrentDictionary<string, string[]> s_pathSegmentsCache = new ConcurrentDictionary<string, string[]>();
|
|
|
|
|
|
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<File>(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<T>(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<T>(pathSegments, index + 1);
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// We're at the final segment, get the property directly
|
|
return ViewModelInstancePropertyHandlersFactory.GetPrimitiveProperty<T>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a nested view model instance property.
|
|
/// </summary>
|
|
/// <param name="path"> The path to the nested property. If the property is on the current instance, the path is the property name. </param>
|
|
/// <returns> The nested view model instance property. </returns>
|
|
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);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Replaces a nested view model instance property with a new instance.
|
|
/// </summary>
|
|
/// <param name="name"> The name of the property to replace.</param>
|
|
/// <param name="value"> The new view model instance to replace the property with.</param>
|
|
/// <returns> 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.</returns>
|
|
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<ViewModelInstance>(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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by a property when it transitions from non-zero to zero subscribers.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a cached view model instance for a given pointer.
|
|
/// /// </summary>
|
|
internal static void RemoveGloballyCachedViewModelInstanceForPointer(IntPtr ptr)
|
|
{
|
|
ViewModelInstanceProperty.RemoveCachedPropertyForPointer(ptr);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="ptr"></param>
|
|
/// <param name="instance"></param>
|
|
internal static void AddCachedViewModelInstanceForPointer(IntPtr ptr, ViewModelInstance instance)
|
|
{
|
|
ViewModelInstanceProperty.AddGloballyCachedVMPropertyForPointer(ptr, instance);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Gets a property of the view model instance.
|
|
/// </summary>
|
|
/// <typeparam name="T"> The type of the property to get. </typeparam>
|
|
/// <param name="path"> The path to the property. If the property is on the current instance, the path is the property name. </param>
|
|
/// <remarks> The path can be a nested path, e.g. "nestedInstance/propertyName". </remarks>
|
|
/// <returns> The property of the view model instance. </returns>
|
|
public T GetProperty<T>(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<T>(this, path);
|
|
}
|
|
|
|
string[] pathSegments = GetPathSegments(path);
|
|
return GetPropertyFromPathSegments<T>(pathSegments, 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects property value changes
|
|
/// Call this after advancing wherever you handle your per-frame logic.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces a nested view model instance property with a new instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property to replace.</param>
|
|
/// <param name="newInstance">The new instance to replace the property with.</param>
|
|
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
|
|
/// <summary>
|
|
/// Gets a number property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The number property, or null if the property doesn't exist or is not a number.</returns>
|
|
public ViewModelInstanceNumberProperty GetNumberProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceNumberProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a boolean property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The boolean property, or null if the property doesn't exist or is not a boolean.</returns>
|
|
public ViewModelInstanceBooleanProperty GetBooleanProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceBooleanProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a string property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The string property, or null if the property doesn't exist or is not a string.</returns>
|
|
public ViewModelInstanceStringProperty GetStringProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceStringProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a color property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The color property, or null if the property doesn't exist or is not a color.</returns>
|
|
public ViewModelInstanceColorProperty GetColorProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceColorProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an enum property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The enum property, or null if the property doesn't exist or is not an enum.</returns>
|
|
public ViewModelInstanceEnumProperty GetEnumProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceEnumProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a trigger property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The trigger property, or null if the property doesn't exist or is not a trigger.</returns>
|
|
public ViewModelInstanceTriggerProperty GetTriggerProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceTriggerProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an image property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The image property, or null if the property doesn't exist or is not an image.</returns>
|
|
public ViewModelInstanceImageProperty GetImageProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceImageProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a list property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The list property, or null if the property doesn't exist or is not a list.</returns>
|
|
public ViewModelInstanceListProperty GetListProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceListProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an artboard property of the view model instance.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The artboard property, or null if the property doesn't exist or is not an artboard.</returns>
|
|
public ViewModelInstanceArtboardProperty GetArtboardProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstanceArtboardProperty>(path);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a nested view model instance property.
|
|
/// </summary>
|
|
/// <param name="path">The path to the property.</param>
|
|
/// <returns>The nested view model instance, or null if the property doesn't exist or is not a view model.</returns>
|
|
public ViewModelInstance GetViewModelInstanceProperty(string path)
|
|
{
|
|
return GetProperty<ViewModelInstance>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="instancePtr"> The native pointer to the ViewModelInstance.</param>
|
|
/// <param name="riveFile"> The Rive file associated with the ViewModelInstance. This is used to resolve the file context for the instance.</param>
|
|
/// <param name="parent"> The parent ViewModelInstance, if any. The parent is used to propagate callbacks to this instance. A vm instance can have multiple parents.</param>
|
|
/// <returns>The ViewModelInstance associated with the native pointer.</returns>
|
|
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);
|
|
|
|
|
|
/// <summary>
|
|
/// Replaces a nested view model instance property with a new instance.
|
|
/// </summary>
|
|
/// <param name="baseInstanceValue">The instance that contains the property to replace.</param>
|
|
/// <param name="path">The path to the property to replace.</param>
|
|
/// <param name="newInstance">The new instance to replace the property with.</param>
|
|
/// <returns>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.</returns>
|
|
[DllImport(NativeLibrary.name)]
|
|
private static extern bool replaceViewModelInstanceViewModelProperty(
|
|
ViewModelInstanceSafeHandle baseInstanceValue,
|
|
string path,
|
|
ViewModelInstanceSafeHandle newInstance);
|
|
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// SafeHandle implementation for ViewModelInstance native resources
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|